Merge remote-tracking branch 'origin/master' into fix-573

This commit is contained in:
Kazuki Nishikawa 2019-11-01 12:19:01 +09:00
commit a3f20c05ea
88 changed files with 16357 additions and 16608 deletions

View file

@ -1,11 +1,17 @@
{
"presets": [
"@babel/preset-env"
],
"plugins": [
"@babel/plugin-transform-modules-commonjs",
["@babel/plugin-transform-spread", {
"loose": true
}]
]
"presets": [["@babel/preset-env", { "loose": true }]],
"env": {
"test": {
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": true
}
}
]
]
}
}
}

View file

@ -1,2 +1 @@
> 5%
IE 11
> 1%

4
.codecov.yml Normal file
View file

@ -0,0 +1,4 @@
coverage:
parsers:
javascript:
enable_partials: yes

View file

@ -1 +1,3 @@
node_modules/
types/
public/

View file

@ -1,54 +0,0 @@
{
"parser": "babel-eslint",
"extends": [
"airbnb",
"prettier"
],
"plugins": [
"prettier",
"cypress"
],
"env": {
"es6": true,
"browser": true,
"node": true,
"mocha": true,
"cypress/globals": true
},
"globals": {
"describe": true,
"it": true,
"before": true,
"after": true,
"beforeEach": true,
"afterEach": true
},
"rules": {
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": true
}
],
"no-console": [
"warn",
{
"allow": [
"warn",
"error"
]
}
],
"no-plusplus": "off",
"no-unused-expressions": "off",
"no-underscore-dangle": "off",
"consistent-return": "off",
"prettier/prettier": [
"error",
{
"singleQuote": true,
"trailingComma": "all"
}
]
}
}

95
.eslintrc.json Normal file
View file

@ -0,0 +1,95 @@
{
"parserOptions": {
"ecmaVersion": 2020
},
"extends": [
"airbnb-base",
"plugin:prettier/recommended",
"plugin:compat/recommended"
],
"plugins": ["prettier", "sort-class-members"],
"env": {
"es6": true,
"browser": true
},
"rules": {
"import/prefer-default-export": "off",
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": true
}
],
"no-console": [
"warn",
{
"allow": ["warn", "error"]
}
],
"no-plusplus": "off",
"no-unused-expressions": "off",
"no-underscore-dangle": "off",
"consistent-return": "off",
"import/no-useless-path-segments": "warn",
"prefer-destructuring": [
"warn",
{
"array": false,
"object": true
}
],
"curly": ["error", "all"],
"newline-before-return": "error",
"sort-class-members/sort-class-members": [
2,
{
"order": [
"[static-properties]",
"[static-methods]",
"[properties]",
"[conventional-private-properties]",
"constructor",
"[methods]",
"[conventional-private-methods]"
],
"accessorPairPositioning": "getThenSet"
}
]
},
"overrides": [
{
"files": ["*.test.js"],
"env": {
"mocha": true
},
"rules": {
"no-restricted-syntax": "off",
"compat/compat": "off",
"no-new": "off"
}
},
{
"files": ["cypress/**"],
"plugins": ["cypress"],
"rules": {
"no-unused-vars": "warn"
},
"env": {
"cypress/globals": true
}
}
],
"settings": {
"polyfills": [
"Array.from",
"Array.prototype.find",
"Array.prototype.includes",
"Symbol",
"Symbol.iterator",
"Object.assign",
"CustomEvent",
"Element.prototype.classList",
"Element.prototype.closest"
]
}
}

View file

@ -1,28 +1,31 @@
<!--- Provide a general summary of your changes in the Title above -->
## Description
<!--- Describe your changes in detail -->
## Motivation and Context
<!--- Describe your changes in detail -->
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->
## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, tests ran to see how -->
<!--- your change affects other areas of the code, etc. -->
## Screenshots (if appropriate):
## Screenshots (if appropriate)
## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Chore (tooling change or documentation change)
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
## Checklist:
## Checklist
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [ ] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [ ] I have updated the documentation accordingly.

View file

@ -0,0 +1,14 @@
const { readFileSync } = require('fs');
const path = require('path');
const assert = require('assert');
const readme = readFileSync(path.resolve(__dirname, '../../README.md'), 'utf8');
const polyfillsFromDocs = /^```polyfills\s*\n([^`]+)\n^```/m
.exec(readme)[1]
.split('\n')
.map(v => v.trim())
.sort();
// @ts-ignore
const polyfillsFromSettings = require('../../.eslintrc.json').settings.polyfills.sort();
assert.deepStrictEqual(polyfillsFromDocs, polyfillsFromSettings);

26
.github/release-drafter.yml vendored Normal file
View file

@ -0,0 +1,26 @@
name-template: 'Draft (next release)'
tag-template: 'v$NEXT_PATCH_VERSION'
sort-direction: descending
categories:
- title: '🚨 Breaking changes'
labels:
- 'breaking change'
- title: '🚀 Features'
labels:
- 'feature'
- 'enhancement'
- title: '🐛 Bug Fixes'
labels:
- 'bugfix'
- title: '🔧 Maintenance'
labels:
- 'chore'
- 'housekeeping'
- 'refactor'
- 'documentation'
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
template: |
# Changes
$CHANGES
# Contributors
$CONTRIBUTORS

20
.github/stale.yml vendored
View file

@ -1,20 +0,0 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 100
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 10
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
- feature request
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
Thanks for contributing to this issue. As it has been 60 days since the last
activity, this issue is being automatically closed. This is often because the
request was already solved in some way and it just wasn't updated or it's no
longer applicable. If that's not the case, please do feel free to either
reopen this issue or open a new one. We'll gladly take a look again!
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

46
.github/workflows/build-and-test.yml vendored Normal file
View file

@ -0,0 +1,46 @@
name: Build and test
on:
push:
branches:
- master
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 1
- uses: actions/setup-node@v1
with:
node-version: 10
# run all tests
- run: |
npm ci
npm run build
npx bundlesize
npm run test:unit:coverage
npm run test:e2e
env:
CI: true
CI_REPO_NAME: ${{ github.event.repository.name }}
CI_REPO_OWNER: ${{ github.event.organization.login }}
CI_COMMIT_SHA: ${{ github.sha }}
GIT_COMMIT: ${{ github.sha }}
CI_BRANCH: ${{ github.head_ref }}
BUNDLESIZE_GITHUB_TOKEN: ${{secrets.BUNDLESIZE_GITHUB_TOKEN}}
FORCE_COLOR: 2
HUSKY_SKIP_INSTALL: true
- name: Upload coverage to Codecov
run: bash <(curl -s https://codecov.io/bash)
-f ./coverage/lcov.info
-B ${{ github.head_ref }}
-C ${{ github.sha }}
-Z || echo 'Codecov upload failed'
env:
CI: true
GITLAB_CI: true # pretend we are GitLab CI, while Codecov adding support for Github Actions
CODECOV_ENV: github-action
CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}}

42
.github/workflows/bundlesize.yml vendored Normal file
View file

@ -0,0 +1,42 @@
name: Bundle size checks
on:
pull_request:
paths:
- 'src/scripts/**'
- 'src/styles/**'
- 'package-lock.json'
- '.browserslistrc'
jobs:
measure:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 1
- uses: actions/setup-node@v1
with:
node-version: 10
- name: Install dependencies and build
run: |
npm ci
npm run build
env:
CYPRESS_INSTALL_BINARY: 0
HUSKY_SKIP_INSTALL: true
# we don't need to build here, as even minized assets expected to be commited
- run: npx bundlesize
env:
CI: true
BUNDLESIZE_GITHUB_TOKEN: ${{secrets.BUNDLESIZE_GITHUB_TOKEN}}
CI_REPO_NAME: ${{ github.event.repository.name }}
CI_REPO_OWNER: ${{ github.event.organization.login }}
CI_COMMIT_SHA: ${{ github.event.after }}
GIT_COMMIT: ${{ github.event.after }}
CI_BRANCH: ${{ github.head_ref }}
FORCE_COLOR: 2

87
.github/workflows/deployment.yml vendored Normal file
View file

@ -0,0 +1,87 @@
name: Publish and deploy
on:
release:
types: [published]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 1
- uses: actions/setup-node@v1
with:
node-version: 10
# run all tests
- run: |
npm ci
npm run build
npx bundlesize
npm run test:unit:coverage
npm run test:e2e
env:
CI: true
CI_REPO_NAME: ${{ github.event.repository.name }}
CI_REPO_OWNER: ${{ github.event.organization.login }}
CI_COMMIT_SHA: ${{ github.sha }}
GIT_COMMIT: ${{ github.sha }}
CI_BRANCH: ${{ github.head_ref }}
BUNDLESIZE_GITHUB_TOKEN: ${{secrets.BUNDLESIZE_GITHUB_TOKEN}}
FORCE_COLOR: 2
HUSKY_SKIP_INSTALL: true
- name: Upload coverage to Codecov
run: bash <(curl -s https://codecov.io/bash)
-f ./coverage/lcov.info
-B ${{ github.head_ref }}
-C ${{ github.sha }}
-Z || echo 'Codecov upload failed'
env:
CI: true
GITLAB_CI: true # pretend we are GitLab CI, while Codecov adding support for Github Actions
CODECOV_ENV: github-action
CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}}
publish-npm:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 1
- uses: actions/setup-node@v1
with:
node-version: 10
registry-url: https://registry.npmjs.org/
- run: npm ci
env:
CYPRESS_INSTALL_BINARY: 0
HUSKY_SKIP_INSTALL: true
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
deploy-gh-pages:
needs: publish-npm
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 1
- uses: actions/setup-node@v1
with:
node-version: 10
registry-url: https://registry.npmjs.org/
- name: Build
run: |
npm ci
npm run build
rm -rf public/test
- name: Deploy
uses: peaceiris/actions-gh-pages@v2.5.0
env:
ACTIONS_DEPLOY_KEY: ${{ secrets.ACTIONS_DEPLOY_KEY }}
PUBLISH_BRANCH: gh-pages
PUBLISH_DIR: ./public

43
.github/workflows/e2e-tests.yml vendored Normal file
View file

@ -0,0 +1,43 @@
name: End-to-end tests
on:
pull_request:
paths:
- 'src/**'
- 'package-lock.json'
- '.browserslistrc'
- '.babelrc'
- 'webpack.config.*'
- 'public/test/**'
- 'cypress/**'
- '.github/workflows/cypress.yml'
jobs:
test-e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 1
- uses: actions/setup-node@v1
with:
node-version: 10
- name: Install dependencies
run: npm ci
env:
HUSKY_SKIP_INSTALL: true
- name: run Cypress CI
run: npx run-p --race start cypress:ci
env:
CI: true
TERM: xterm-256color
NODE_ENV: production # prevent watching
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
DEBUG: commit-info,cypress:server:record
# https://docs.cypress.io/guides/guides/continuous-integration.html#Environment-variables
COMMIT_INFO_BRANCH: ${{ github.head_ref }}
COMMIT_INFO_AUTHOR: ${{ github.event.sender.login }}
COMMIT_INFO_SHA: ${{ github.event.after }}

38
.github/workflows/lint.yml vendored Normal file
View file

@ -0,0 +1,38 @@
name: Code linting
on:
pull_request:
paths:
- 'src/scripts/**'
- package-lock.json
- '.browserslistrc'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 1
- uses: actions/setup-node@v1
with:
node-version: 10
- name: Install dependencies
run: npm install --no-optional --no-audit --ignore-scripts
env:
CYPRESS_INSTALL_BINARY: 0
HUSKY_SKIP_INSTALL: true
- name: run eslint
run: |
CHANGED_JS=$(git --no-pager diff --name-only ..origin/master | grep '^src\/scripts\/.*\.js$' | xargs ls -d 2>/dev/null | paste -sd " " -)
if [[ -z $(sed -e 's/[[:space:]]*$//' <<<${CHANGED_JS}) ]]; then CHANGED_JS="src/scripts"; fi
echo $CHANGED_JS
node node_modules/eslint/bin/eslint.js $CHANGED_JS
- name: Lint JS bundle
run: |
npm run js:build
npx eslint --no-ignore ./public/assets/scripts/*.js

23
.github/workflows/polyfills-sync.yml vendored Normal file
View file

@ -0,0 +1,23 @@
name: Polyfills documentation
on:
pull_request:
paths:
- 'README.md'
- '.browserslistrc'
- '.eslintrc.json'
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 1
- uses: actions/setup-node@v1
with:
node-version: 12
- name: Check Polyfills documentation and settings sync
run: node .github/actions-scripts/polyfills-sync.js

View file

@ -0,0 +1,14 @@
name: Release management
on:
push:
branches:
- master
jobs:
update-draft-release:
runs-on: ubuntu-latest
steps:
- uses: toolmantim/release-drafter@v5.2.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

27
.github/workflows/types.yml vendored Normal file
View file

@ -0,0 +1,27 @@
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

42
.github/workflows/unit-tests.yml vendored Normal file
View file

@ -0,0 +1,42 @@
name: Unit tests
on:
pull_request:
paths:
- 'src/scripts/**'
- package-lock.json
- '.browserslistrc'
jobs:
test-unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 1
- uses: actions/setup-node@v1
with:
node-version: 10
- name: Install dependencies
run: npm install --no-optional --no-audit --ignore-scripts
env:
CYPRESS_INSTALL_BINARY: 0
HUSKY_SKIP_INSTALL: true
- run: npm run test:unit:coverage
env:
FORCE_COLOR: 2
- name: Upload coverage to Codecov
run: bash <(curl -s https://codecov.io/bash)
-f ./coverage/lcov.info
-B ${{ github.head_ref }}
-C ${{ github.sha }}
-Z || echo 'Codecov upload failed'
env:
CI: true
GITLAB_CI: true # pretend we are GitLab CI, while Codecov adding support for Github Actions
CODECOV_ENV: github-action
CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}}

6
.huskyrc Normal file
View file

@ -0,0 +1,6 @@
{
"skipCI": true,
"hooks": {
"pre-commit": "lint-staged"
}
}

7
.mocharc.yml Normal file
View file

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

2
.npmrc Normal file
View file

@ -0,0 +1,2 @@
message=":bookmark: Version %s"
git-tag-version=true

13
.prettierrc.json Normal file
View file

@ -0,0 +1,13 @@
{
"singleQuote": true,
"trailingComma": "all",
"overrides": [
{
"files": ["*.svg"],
"options": {
"parser": "html",
"htmlWhitespaceSensitivity": "ignore"
}
}
]
}

View file

@ -1,19 +0,0 @@
language: node_js
node_js:
- 10
cache:
directories:
- ~/.npm
- ~/.cache
install:
- npm ci
jobs:
include:
- stage: Judging bundle size
script: npm run bundlesize
- stage: Linting code
script: npm run lint
- stage: Running tests
script: npm run test

17
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,17 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
// we enforce ESLint rules, so, recommend extension
"dbaeumer.vscode-eslint",
// we use prettier, so, recommend extension
"esbenp.prettier-vscode",
// we are on GitHub, so, recommend extension
"github.vscode-pull-request-github",
// needed for our configured debug configuration with Chrome
"msjsdiag.debugger-for-chrome"
// Mocha recommended - https://mochajs.org/#mocha-sidebar-vs-code,
// but it's buggy
// "maty.vscode-mocha-sidebar"
]
}

70
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,70 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome",
"preLaunchTask": "buildAndWatch",
"url": "http://localhost:3001",
"webRoot": "${workspaceFolder}",
"sourceMapPathOverrides": {
"webpack://Choices/*": "${workspaceFolder}/*"
}
},
{
"type": "node",
"request": "launch",
"name": "Mocha Current File",
"program": "${workspaceFolder}/node_modules/mocha/bin/mocha",
"args": ["-u", "bdd", "--timeout", "999999", "--colors", "${file}"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"env": {
"NODE_ENV": "test"
}
},
{
"type": "node",
"request": "launch",
"name": "Mocha All",
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
"args": ["-u", "bdd", "--timeout", "999999", "--colors"],
"console": "integratedTerminal",
"internalConsoleOptions": "openOnSessionStart",
"env": {
"NODE_ENV": "test"
}
},
{
"type": "node",
"request": "launch",
"name": "Cypress Current File",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/cypress",
"windows": {
"runtimeExecutable": "${workspaceFolder}\\node_modules\\.bin\\cypress.cmd"
},
"runtimeArgs": [
"run",
"--headed",
"--no-exit",
"--browser=electron",
"--port",
"9898",
"--spec"
],
"protocol": "legacy",
"port": 9898,
"program": "${file}",
"console": "integratedTerminal",
"preLaunchTask": "buildAndWatch",
"internalConsoleOptions": "openOnSessionStart",
"timeout": 999999999999999,
"autoAttachChildProcesses": false,
"env": {
"NODE_ENV": "test"
// "DEBUG": "cypress:*"
}
}
]
}

64
.vscode/settings.json vendored
View file

@ -1,3 +1,63 @@
{
"eslint.enable": true
}
"eslint.enable": true,
// prevent watch task failures on lint errors
"eslint.autoFixOnSave": true,
// switch off default VSCode formatting rules
"javascript.format.enable": false,
// Javascript prettier runs via ESLint
"prettier.disableLanguages": ["javascript"],
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.formatOnSave": false
},
"search.exclude": {
"**/node_modules": true,
"public/assets": true,
"**/coverage": true
},
// Mocha Sidebar settings
"mocha.env": {
"NODE_ENV": "test"
},
"mocha.files.glob": "src/scripts/**/*.test.js",
"mocha.requires": ["@babel/register", "./config/jsdom.js"],
// for Windows collaborators
"files.eol": "\n",
"files.encoding": "utf8",
// associations for some files this project is using
"files.associations": {
".browserslistrc": "gitignore",
".huskyrc": "jsonc",
".npmrc": "ini"
},
// We use NPM as package manager
"npm.packageManager": "npm",
"npm.autoDetect": "on",
"npm.fetchOnlinePackageInfo": true,
"eslint.packageManager": "npm",
"json.schemas": [
// Cypress related settings - https://docs.cypress.io/guides/tooling/intelligent-code-completion.html#Features-1
{
"fileMatch": ["cypress.json"],
"url": "https://on.cypress.io/cypress.schema.json"
},
// Husky config file
{
"fileMatch": [".huskyrc"],
"url": "http://json.schemastore.org/huskyrc"
},
// Prettier config
{
"fileMatch": [".prettierrc.json"],
"url": "http://json.schemastore.org/prettierrc"
}
]
}

87
.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,87 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"label": "buildAndWatch",
"script": "js:watch",
"group": {
"kind": "build",
"isDefault": true
},
"isBackground": true,
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "dedicated",
"showReuseMessage": true,
"clear": false
},
"problemMatcher": [
"$eslint-stylish",
{
"owner": "webpack",
"fileLocation": "absolute",
"pattern": [
{
"regexp": "^Module build failed \\(from (\\.+)\\)",
"file": 1,
"line": 2,
"column": 3
},
{
"regexp": "\\s*TS\\d+:\\s*(.*)",
"message": 1
}
],
"severity": "error",
"source": "webpack",
"background": {
"activeOnStart": true,
"beginsPattern": "^Listening at",
"endsPattern": "Compiled successfully\\."
}
}
]
},
{
"type": "npm",
"script": "css:build",
"group": "build",
"problemMatcher": ["$node-sass"]
},
{
"type": "npm",
"script": "lint",
"problemMatcher": ["$eslint-stylish"]
},
{
"type": "npm",
"script": "build",
"group": "build"
},
{
"type": "npm",
"script": "test",
"group": "test"
},
{
"type": "npm",
"script": "test:e2e",
"group": "test"
},
{
"type": "npm",
"script": "test:unit",
"group": "test"
},
{
"type": "npm",
"script": "cypress:open",
"isBackground": true
}
]
}

View file

@ -1,11 +1,12 @@
# Contributions
# Contributions
In lieu of a formal styleguide, take care to maintain the existing coding style ensuring there are no linting errors. Add unit tests for any new or changed functionality. Lint and test your code using the npm scripts below:
### NPM tasks
| Task | Usage |
| -------------------- | ------------------------------------------------------------ |
| `npm run start` | Fire up local server for development |
| `npm run test` | Run sequence of tests once |
| `npm run test:unit` | Run sequence of unit tests once |
| `npm run test:e2e` | Run sequence of integration tests once |
| `npm run test:watch` | Fire up test server and re-test on file change |
| `npm run js:build` | Compile Choices to an uglified JavaScript file |
| `npm run css:watch` | Watch SCSS files for changes. On a change, run build process |

492
README.md

File diff suppressed because it is too large Load diff

View file

@ -1,54 +0,0 @@
const fs = require('fs');
const path = require('path');
const config = {
files: ['public/index.html'],
};
/**
* Convert node arguments into an object
* @return {Object} Arguments
*/
const argvToObject = () => {
const args = {};
let arg = null;
process.argv.forEach((val, index) => {
if (/^--/.test(val)) {
arg = {
index,
name: val.replace(/^--/, ''),
};
return;
}
if (arg && arg.index + 1 === index) {
args[arg.name] = val;
}
});
return args;
};
/**
* Loop through files updating the current version
* @param {Object} config
*/
const updateVersion = ({ files }) => {
const args = argvToObject();
const version = args.current;
console.log(`Updating version to ${version}`);
files.forEach(file => {
const filePath = path.join(__dirname, file);
const regex = new RegExp(/\?version=(.*?)\"/, 'g');
let contents = fs.readFileSync(filePath, 'utf-8');
contents = contents.replace(regex, `?version=${version}"`);
fs.writeFileSync(filePath, contents);
});
console.log(`Updated version to ${version}`);
};
updateVersion(config);

View file

@ -1,6 +1,13 @@
/* eslint-disable no-param-reassign */
const { JSDOM } = require('jsdom');
const jsdom = new JSDOM('<!doctype html><html><body></body></html>');
const jsdom = new JSDOM(
'<!doctype html><html><head><meta charset="utf-8"></head><body></body></html>',
{
pretendToBeVisual: true,
},
);
const { window } = jsdom;
function copyProps(src, target) {
@ -20,26 +27,6 @@ function ignoreExtensions(extensions = [], returnValue = {}) {
});
}
function mockRAF(global) {
let callbacksQueue = [];
global.setInterval(() => {
for (let i = 0; i < callbacksQueue.length; i++) {
if (callbacksQueue[i] !== false) {
callbacksQueue[i].call(null);
}
}
callbacksQueue = [];
}, 1000 / 60);
global.requestAnimationFrame = callback => callbacksQueue.push(callback) - 1;
global.cancelAnimationFrame = id => {
callbacksQueue[id] = false;
};
}
global.window = window;
global.document = window.document;
global.navigator = {
@ -48,12 +35,16 @@ global.navigator = {
global.CustomEvent = window.CustomEvent;
global.Element = window.Element;
global.HTMLElement = window.HTMLElement;
global.Option = window.Option;
global.HTMLOptionElement = window.HTMLOptionElement;
global.HTMLOptGroupElement = window.HTMLOptGroupElement;
global.HTMLSelectElement = window.HTMLSelectElement;
global.HTMLInputElement = window.HTMLInputElement;
global.DocumentFragment = window.DocumentFragment;
global.requestAnimationFrame = window.requestAnimationFrame;
window.matchMedia = () => true;
copyProps(window, global);
mockRAF(global);
ignoreExtensions(['.scss', '.css']);
ignoreExtensions(['.jpg', '.png', '.svg'], '');

View file

@ -1,4 +1,5 @@
{
"baseUrl": "http://localhost:3001/test",
"video": false
}
"video": false,
"projectId": "n7g5qp"
}

View file

@ -79,7 +79,7 @@ describe('Choices - select multiple', () => {
it('updates the value of the original input', () => {
cy.get('[data-test-hook=basic]')
.find('.choices__input.is-hidden')
.find('.choices__input[hidden]')
.should($select => {
expect($select.val()).to.contain(selectedChoiceText);
});
@ -150,7 +150,7 @@ describe('Choices - select multiple', () => {
it('updates the value of the original input', () => {
cy.get('[data-test-hook=basic]')
.find('.choices__input.is-hidden')
.find('.choices__input[hidden]')
.should($select => {
const val = $select.val() || [];
expect(val).to.not.contain(removedChoiceText);
@ -334,7 +334,7 @@ describe('Choices - select multiple', () => {
});
describe('on click', () => {
it('does not opens choice dropdown', () => {
it('does not open choice dropdown', () => {
cy.get('[data-test-hook=disabled-via-attr]')
.find('.choices')
.click()
@ -515,7 +515,7 @@ describe('Choices - select multiple', () => {
});
describe('on click', () => {
it('does not opens choice dropdown', () => {
it('does not open choice dropdown', () => {
cy.get('[data-test-hook=remote-data]')
.find('.choices')
.click()
@ -567,6 +567,8 @@ describe('Choices - select multiple', () => {
it('scrolls to next choice on down arrow', () => {
for (let index = 0; index < choicesCount; index++) {
cy.wait(100);
cy.get('[data-test-hook=scrolling-dropdown]')
.find('.choices__list--dropdown .choices__list .is-highlighted')
.should($choice => {
@ -753,7 +755,7 @@ describe('Choices - select multiple', () => {
describe('within form', () => {
describe('selecting choice', () => {
describe('on enter key', () => {
it('does not submit form', () => {
it('selects choice', () => {
cy.get('[data-test-hook=within-form] form').then($form => {
$form.submit(() => {
// this will fail the test if the form submits
@ -763,7 +765,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=within-form]')
.find('.choices__input--cloned')
.focus()
.click()
.type('{enter}');
cy.get('[data-test-hook=within-form]')
@ -804,7 +806,7 @@ describe('Choices - select multiple', () => {
it('updates the value of the original input', () => {
cy.get('[data-test-hook=set-choice-by-value]')
.find('.choices__input.is-hidden')
.find('.choices__input[hidden]')
.should($select => {
const val = $select.val() || [];
expect(val).to.contain(dynamicallySelectedChoiceValue);

View file

@ -208,7 +208,7 @@ describe('Choices - select one', () => {
it('updates the value of the original input', () => {
cy.get('[data-test-hook=remove-button]')
.find('.choices__input.is-hidden')
.find('.choices__input[hidden]')
.should($select => {
const val = $select.val() || [];
@ -466,7 +466,7 @@ describe('Choices - select one', () => {
});
describe('on click', () => {
it('does not opens choice dropdown', () => {
it('does not open choice dropdown', () => {
cy.get('[data-test-hook=remote-data]')
.find('.choices')
.click()
@ -818,7 +818,7 @@ describe('Choices - select one', () => {
it('updates the value of the original input', () => {
cy.get('[data-test-hook=set-choice-by-value]')
.find('.choices__input.is-hidden')
.find('.choices__input[hidden]')
.should($select => {
const val = $select.val() || [];
expect(val).to.contain(dynamicallySelectedChoiceValue);

View file

@ -29,7 +29,7 @@ describe('Choices - text element', () => {
.type('{enter}');
cy.get('[data-test-hook=basic]')
.find('.choices__input.is-hidden')
.find('.choices__input[hidden]')
.should('have.value', textInput);
});
@ -151,7 +151,7 @@ describe('Choices - text element', () => {
.click();
cy.get('[data-test-hook=remove-button]')
.find('.choices__input.is-hidden')
.find('.choices__input[hidden]')
.then($input => {
expect($input.val()).to.not.contain(textInput);
});

View file

@ -14,4 +14,4 @@
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}
};

View file

@ -14,7 +14,7 @@
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
import './commands';
// Alternatively you can use CommonJS syntax:
// require('./commands')

16
jsconfig.json Normal file
View file

@ -0,0 +1,16 @@
{
"compilerOptions": {
"checkJs": true,
"target": "es2020",
"lib": ["esnext", "dom"],
"types": ["cypress"],
"strict": true,
"moduleResolution": "node",
/* Additional Checks */
"noImplicitAny": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
}
}

14
lint-staged.config.js Normal file
View file

@ -0,0 +1,14 @@
module.exports = {
'*.js': ['eslint --fix --quiet -f visualstudio', 'git add'],
'*.{ts,scss,yaml,yml,md,html,json,babelrc,eslintrc}': [
'prettier --write',
'git add',
],
'src/icons/*.svg': [
'prettier --write --parser=html --html-whitespace-sensitivity=ignore',
'git add',
],
'.codecov.yml': () =>
'curl -f --silent --data-binary @.codecov.yml https://codecov.io/validate',
'src/scripts/**/*.js': () => 'npm run test:unit',
};

15472
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,32 +1,32 @@
{
"name": "choices.js",
"version": "7.0.0",
"version": "8.0.0",
"description": "A vanilla JS customisable text input/select box plugin",
"main": "./public/assets/scripts/choices.min.js",
"main": "./public/assets/scripts/choices.js",
"types": "./types/index.d.ts",
"scripts": {
"start": "run-p js:watch css:watch",
"build": "npm run js:build && npm run css:build",
"lint": "eslint $(find src -name '*.js')",
"coverage": "nyc npm run test:unit",
"build": "run-p js:build css:build",
"lint": "eslint src/scripts",
"bundlesize": "bundlesize",
"cypress:run": "$(npm bin)/cypress run",
"cypress:open": "$(npm bin)/cypress open",
"test": "npm run test:unit && npm run test:e2e",
"test:unit": "mocha --require ./config/jsdom.js --require @babel/register $(find src -name '*.test.js') --exit",
"test:unit:watch": "npm run test:unit -- --watch --inspect=5556",
"cypress:ci": "cypress run --record --group $GITHUB_REF --ci-build-id $GITHUB_SHA",
"test": "run-s test:unit test:e2e",
"test:unit": "NODE_ENV=test mocha",
"test:unit:watch": "NODE_ENV=test mocha --watch --inspect=5556",
"test:unit:coverage": "NODE_ENV=test nyc --reporter=lcov --reporter=text --reporter=text-summary mocha",
"test:e2e": "run-p --race start cypress:run",
"js:watch": "NODE_ENV=development node server.js",
"js:build": "webpack --config webpack.config.prod.js",
"css:watch": "nodemon -e scss -x \"npm run css:build\"",
"css:build": "run-p css:sass css:prefix css:min",
"css:sass": "node-sass --output-style expanded --include-path scss src/styles/base.scss public/assets/styles/base.css && node-sass --output-style expanded --include-path scss src/styles/choices.scss public/assets/styles/choices.css",
"css:prefix": "postcss --use autoprefixer -b 'last 2 versions' public/assets/styles/*.css -d public/assets/styles",
"css:min": "csso public/assets/styles/base.css public/assets/styles/base.min.css && csso public/assets/styles/choices.css public/assets/styles/choices.min.css",
"bump-cache": "node bump-cache.js --current $npm_package_version",
"css:build": "run-s css:sass css:prefix css:min",
"css:sass": "node-sass --output-style expanded --include-path scss src/styles/base.scss public/assets/styles/base.css && node-sass --output-style expanded --include-path scss src/styles/choices.scss public/assets/styles/choices.css",
"css:prefix": "postcss public/assets/styles/*.css --use autoprefixer --no-map --env prod --dir public/assets/styles",
"css:min": "csso public/assets/styles/base.css --output public/assets/styles/base.min.css && csso public/assets/styles/choices.css --output public/assets/styles/choices.min.css",
"deploy": "git subtree push --prefix public origin gh-pages",
"postversion": "npm run js:build && npm run bump-cache",
"prepush": "run-p lint test:unit && npm run bundlesize"
"postversion": "git push --no-verify --atomic --follow-tags",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
@ -38,6 +38,7 @@
"public/assets/scripts",
"public/assets/styles",
"src",
"!src/**/*.test.js",
"types"
],
"bugs": {
@ -53,53 +54,45 @@
"js"
],
"devDependencies": {
"@babel/core": "^7.2.2",
"@babel/plugin-proposal-class-properties": "^7.3.0",
"@babel/plugin-transform-modules-commonjs": "^7.2.0",
"@babel/plugin-transform-spread": "^7.2.2",
"@babel/preset-env": "^7.3.1",
"@babel/register": "^7.0.0",
"autoprefixer": "^6.3.3",
"babel-eslint": "^9.0.0",
"babel-loader": "^8.0.5",
"bundlesize": "^0.17.1",
"@babel/core": "^7.6.4",
"@babel/preset-env": "^7.6.3",
"@babel/register": "^7.6.2",
"autoprefixer": "^9.6.5",
"babel-loader": "^8.0.6",
"bundlesize": "^0.18.0",
"chai": "^4.2.0",
"csso": "^1.8.2",
"cypress": "^3.1.5",
"eslint": "^3.19.0",
"eslint-config-airbnb": "^15.1.0",
"eslint-config-prettier": "^2.10.0",
"eslint-loader": "^2.1.2",
"eslint-plugin-cypress": "^2.2.0",
"eslint-plugin-import": "^2.16.0",
"eslint-plugin-jsx-a11y": "^5.1.1",
"eslint-plugin-prettier": "^2.7.0",
"eslint-plugin-react": "^7.12.4",
"csso-cli": "^3.0.0",
"cypress": "3.5.0",
"eslint": "^6.6.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-config-prettier": "^6.5.0",
"eslint-loader": "^3.0.2",
"eslint-plugin-compat": "3.3.0",
"eslint-plugin-cypress": "^2.7.0",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-sort-class-members": "^1.6.0",
"express": "^4.16.4",
"husky": "^0.14.3",
"jsdom": "^11.12.0",
"mocha": "^5.2.0",
"node-sass": "^4.11.0",
"husky": "^3.0.9",
"jsdom": "^15.2.0",
"lint-staged": "^9.4.2",
"mocha": "^6.2.2",
"node-sass": "^4.12.0",
"nodemon": "^1.18.10",
"npm-run-all": "^4.1.5",
"nyc": "^11.9.0",
"opn": "^5.4.0",
"postcss-cli": "^2.5.1",
"prettier": "^1.16.4",
"sinon": "^2.4.0",
"unminified-webpack-plugin": "^2.0.0",
"webpack": "^4.29.3",
"webpack-cli": "^3.2.3",
"webpack-dev-middleware": "^3.5.2",
"webpack-hot-middleware": "^2.24.3",
"whatwg-fetch": "^1.0.0",
"wrapper-webpack-plugin": "^2.1.0"
"nyc": "^14.1.1",
"postcss-cli": "^6.1.3",
"prettier": "^1.18.2",
"sinon": "^7.5.0",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.9",
"webpack-dev-middleware": "^3.7.2",
"webpack-hot-middleware": "^2.25.0"
},
"dependencies": {
"classnames": "^2.2.6",
"deepmerge": "^2.2.1",
"fuse.js": "3.4.2",
"redux": "^3.3.1"
"deepmerge": "^4.2.0",
"fuse.js": "^3.4.5",
"redux": "^4.0.4"
},
"npmName": "choices.js",
"npmFileMap": [
@ -122,11 +115,11 @@
"bundlesize": [
{
"path": "public/assets/scripts/choices.min.js",
"maxSize": "25 kB"
"maxSize": "20 kB"
},
{
"path": "public/assets/styles/choices.min.css",
"maxSize": "2 kB"
"maxSize": "1.8 kB"
}
]
}

View file

@ -0,0 +1,24 @@
// get polyfill settings from top level config
// @ts-ignore
const { settings } = require('../../../.eslintrc.json');
// Adding non-polyfilable Symbol-related functions as they are most probably
// behind the flag
settings.polyfills.push('Symbol.toStringTag', 'Symbol.for', 'Object.getOwnPropertySymbols', 'Object.getOwnPropertyDescriptors')
module.exports = /** @type {import('eslint').Linter.Config} */({
root: true,
extends: [
"plugin:compat/recommended"
],
parserOptions: {
// ensure that it's compatible with ES5 browsers, so, no `const`, etc
ecmaVersion: 5
},
env: {
browser: true
},
settings
})

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -21,10 +21,10 @@ body {
}
body {
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
font-family: 'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif;
font-size: 16px;
line-height: 1.4;
color: #FFFFFF;
color: #ffffff;
background-color: #333;
overflow-x: hidden;
}
@ -64,7 +64,7 @@ h6 {
a,
a:visited,
a:focus {
color: #FFFFFF;
color: #ffffff;
text-decoration: none;
font-weight: 600;
}
@ -135,7 +135,7 @@ label + p {
}
.section {
background-color: #FFFFFF;
background-color: #ffffff;
padding: 24px;
color: #333;
}
@ -180,10 +180,6 @@ label + p {
text-align: center;
}
.is-hidden {
display: none;
}
[data-test-hook] {
margin-bottom: 24px;
}

View file

@ -1 +1 @@
*{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}*,:after,:before{box-sizing:border-box}body,html{position:relative;margin:0;width:100%;height:100%}body{font-family:"Helvetica Neue",Helvetica,Arial,"Lucida Grande",sans-serif;font-size:16px;line-height:1.4;color:#fff;background-color:#333;overflow-x:hidden}hr,label{display:block}label,p{margin-bottom:8px}label{font-size:14px;font-weight:500;cursor:pointer}p{margin-top:0}hr{margin:30px 0;border:0;border-bottom:1px solid #eaeaea;height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:12px;font-weight:400;line-height:1.2}a,a:focus,a:visited{color:#fff;text-decoration:none;font-weight:600}.form-control{display:block;width:100%;background-color:#f9f9f9;padding:12px;border:1px solid #ddd;border-radius:2.5px;font-size:14px;-webkit-appearance:none;-moz-appearance:none;appearance:none;margin-bottom:24px}.h1,h1{font-size:32px}.h2,h2{font-size:24px}.h3,h3{font-size:20px}.h4,h4{font-size:18px}.h5,h5{font-size:16px}.h6,h6{font-size:14px}label+p{margin-top:-4px}.container{display:block;margin:auto;max-width:40em;padding:48px}@media (max-width:620px){.container{padding:0}}.section{background-color:#fff;padding:24px;color:#333}.section a,.section a:focus,.section a:visited{color:#00bcd4}.logo{display:block;margin-bottom:12px}.logo__img{width:100%;height:auto;display:inline-block;max-width:100%;vertical-align:top;padding:6px 0}.visible-ie{display:none}.push-bottom{margin-bottom:24px}.zero-bottom{margin-bottom:0}.zero-top{margin-top:0}.text-center{text-align:center}.is-hidden{display:none}[data-test-hook]{margin-bottom:24px}
*{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}*,:after,:before{box-sizing:border-box}body,html{position:relative;margin:0;width:100%;height:100%}body{font-family:'Helvetica Neue',Helvetica,Arial,'Lucida Grande',sans-serif;font-size:16px;line-height:1.4;color:#fff;background-color:#333;overflow-x:hidden}hr,label{display:block}label,p{margin-bottom:8px}label{font-size:14px;font-weight:500;cursor:pointer}p{margin-top:0}hr{margin:30px 0;border:0;border-bottom:1px solid #eaeaea;height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:12px;font-weight:400;line-height:1.2}a,a:focus,a:visited{color:#fff;text-decoration:none;font-weight:600}.form-control{display:block;width:100%;background-color:#f9f9f9;padding:12px;border:1px solid #ddd;border-radius:2.5px;font-size:14px;-webkit-appearance:none;-moz-appearance:none;appearance:none;margin-bottom:24px}.h1,h1{font-size:32px}.h2,h2{font-size:24px}.h3,h3{font-size:20px}.h4,h4{font-size:18px}.h5,h5{font-size:16px}.h6,h6{font-size:14px}label+p{margin-top:-4px}.container{display:block;margin:auto;max-width:40em;padding:48px}@media (max-width:620px){.container{padding:0}}.section{background-color:#fff;padding:24px;color:#333}.section a,.section a:focus,.section a:visited{color:#00bcd4}.logo{display:block;margin-bottom:12px}.logo__img{width:100%;height:auto;display:inline-block;max-width:100%;vertical-align:top;padding:6px 0}.visible-ie{display:none}.push-bottom{margin-bottom:24px}.zero-bottom{margin-bottom:0}.zero-top{margin-top:0}.text-center{text-align:center}[data-test-hook]{margin-bottom:24px}

View file

@ -17,33 +17,39 @@
.choices.is-disabled .choices__inner,
.choices.is-disabled .choices__input {
background-color: #EAEAEA;
background-color: #eaeaea;
cursor: not-allowed;
user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.choices.is-disabled .choices__item {
cursor: not-allowed;
}
.choices[data-type*="select-one"] {
.choices [hidden] {
display: none !important;
}
.choices[data-type*='select-one'] {
cursor: pointer;
}
.choices[data-type*="select-one"] .choices__inner {
.choices[data-type*='select-one'] .choices__inner {
padding-bottom: 7.5px;
}
.choices[data-type*="select-one"] .choices__input {
.choices[data-type*='select-one'] .choices__input {
display: block;
width: 100%;
padding: 10px;
border-bottom: 1px solid #DDDDDD;
background-color: #FFFFFF;
border-bottom: 1px solid #dddddd;
background-color: #ffffff;
margin: 0;
}
.choices[data-type*="select-one"] .choices__button {
.choices[data-type*='select-one'] .choices__button {
background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==);
padding: 0;
background-size: 8px;
@ -55,19 +61,19 @@
height: 20px;
width: 20px;
border-radius: 10em;
opacity: .5;
opacity: 0.5;
}
.choices[data-type*="select-one"] .choices__button:hover, .choices[data-type*="select-one"] .choices__button:focus {
.choices[data-type*='select-one'] .choices__button:hover, .choices[data-type*='select-one'] .choices__button:focus {
opacity: 1;
}
.choices[data-type*="select-one"] .choices__button:focus {
box-shadow: 0px 0px 0px 2px #00BCD4;
.choices[data-type*='select-one'] .choices__button:focus {
box-shadow: 0px 0px 0px 2px #00bcd4;
}
.choices[data-type*="select-one"]:after {
content: "";
.choices[data-type*='select-one']:after {
content: '';
height: 0;
width: 0;
border-style: solid;
@ -80,30 +86,30 @@
pointer-events: none;
}
.choices[data-type*="select-one"].is-open:after {
.choices[data-type*='select-one'].is-open:after {
border-color: transparent transparent #333333 transparent;
margin-top: -7.5px;
}
.choices[data-type*="select-one"][dir="rtl"]:after {
.choices[data-type*='select-one'][dir='rtl']:after {
left: 11.5px;
right: auto;
}
.choices[data-type*="select-one"][dir="rtl"] .choices__button {
.choices[data-type*='select-one'][dir='rtl'] .choices__button {
right: auto;
left: 0;
margin-left: 25px;
margin-right: 0;
}
.choices[data-type*="select-multiple"] .choices__inner,
.choices[data-type*="text"] .choices__inner {
.choices[data-type*='select-multiple'] .choices__inner,
.choices[data-type*='text'] .choices__inner {
cursor: text;
}
.choices[data-type*="select-multiple"] .choices__button,
.choices[data-type*="text"] .choices__button {
.choices[data-type*='select-multiple'] .choices__button,
.choices[data-type*='text'] .choices__button {
position: relative;
display: inline-block;
margin-top: 0;
@ -116,13 +122,13 @@
background-size: 8px;
width: 8px;
line-height: 1;
opacity: .75;
opacity: 0.75;
border-radius: 0;
}
.choices[data-type*="select-multiple"] .choices__button:hover, .choices[data-type*="select-multiple"] .choices__button:focus,
.choices[data-type*="text"] .choices__button:hover,
.choices[data-type*="text"] .choices__button:focus {
.choices[data-type*='select-multiple'] .choices__button:hover, .choices[data-type*='select-multiple'] .choices__button:focus,
.choices[data-type*='text'] .choices__button:hover,
.choices[data-type*='text'] .choices__button:focus {
opacity: 1;
}
@ -132,7 +138,7 @@
width: 100%;
background-color: #f9f9f9;
padding: 7.5px 7.5px 3.75px;
border: 1px solid #DDDDDD;
border: 1px solid #dddddd;
border-radius: 2.5px;
font-size: 14px;
min-height: 44px;
@ -164,7 +170,7 @@
width: 100%;
}
[dir="rtl"] .choices__list--single {
[dir='rtl'] .choices__list--single {
padding-right: 4px;
padding-left: 16px;
}
@ -186,9 +192,9 @@
font-weight: 500;
margin-right: 3.75px;
margin-bottom: 3.75px;
background-color: #00BCD4;
background-color: #00bcd4;
border: 1px solid #00a5bb;
color: #FFFFFF;
color: #ffffff;
word-break: break-all;
}
@ -196,7 +202,7 @@
padding-right: 5px;
}
[dir="rtl"] .choices__list--multiple .choices__item {
[dir='rtl'] .choices__list--multiple .choices__item {
margin-right: 0;
margin-left: 3.75px;
}
@ -216,8 +222,8 @@
z-index: 1;
position: absolute;
width: 100%;
background-color: #FFFFFF;
border: 1px solid #DDDDDD;
background-color: #ffffff;
border: 1px solid #dddddd;
top: 100%;
margin-top: -1px;
border-bottom-left-radius: 2.5px;
@ -239,7 +245,7 @@
bottom: 100%;
margin-top: 0;
margin-bottom: -1px;
border-radius: .25rem .25rem 0 0;
border-radius: 0.25rem 0.25rem 0 0;
}
.choices__list--dropdown .choices__list {
@ -256,7 +262,7 @@
font-size: 14px;
}
[dir="rtl"] .choices__list--dropdown .choices__item {
[dir='rtl'] .choices__list--dropdown .choices__item {
text-align: right;
}
@ -273,12 +279,12 @@
top: 50%;
transform: translateY(-50%);
}
[dir="rtl"] .choices__list--dropdown .choices__item--selectable {
[dir='rtl'] .choices__list--dropdown .choices__item--selectable {
text-align: right;
padding-left: 100px;
padding-right: 10px;
}
[dir="rtl"] .choices__list--dropdown .choices__item--selectable:after {
[dir='rtl'] .choices__list--dropdown .choices__item--selectable:after {
right: auto;
left: 10px;
}
@ -289,7 +295,7 @@
}
.choices__list--dropdown .choices__item--selectable.is-highlighted:after {
opacity: .5;
opacity: 0.5;
}
.choices__item {
@ -302,8 +308,10 @@
.choices__item--disabled {
cursor: not-allowed;
user-select: none;
opacity: .5;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
opacity: 0.5;
}
.choices__heading {
@ -317,7 +325,8 @@
.choices__button {
text-indent: -9999px;
-webkit-appearance: none;
appearance: none;
-moz-appearance: none;
appearance: none;
border: 0;
background-color: transparent;
background-repeat: no-repeat;
@ -345,19 +354,13 @@
outline: 0;
}
[dir="rtl"] .choices__input {
[dir='rtl'] .choices__input {
padding-right: 2px;
padding-left: 0;
}
.choices__placeholder {
opacity: .5;
}
.choices__input.is-hidden,
.choices[data-type*="select-one"] .choices__input.is-hidden,
.choices[data-type*="select-multiple"] .choices__input.is-hidden {
display: none;
opacity: 0.5;
}
/*===== End of Choices ======*/

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -1,28 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width,initial-scale=1,user-scalable=no"
/>
<title>Choices</title>
<meta
name="description"
itemprop="description"
content="A lightweight, configurable select box/text input plugin. Similar to Select2 and Selectize but without the jQuery dependency."
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="../assets/images/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
href="../assets/images/favicon-32x32.png"
sizes="32x32"
/>
<link
rel="icon"
type="image/png"
href="../assets/images/favicon-16x16.png"
sizes="16x16"
/>
<link rel="manifest" href="../assets/images/manifest.json" />
<link
rel="mask-icon"
href="../assets/images/safari-pinned-tab.svg"
color="#00bcd4"
/>
<link rel="shortcut icon" href="../assets/images/favicon.ico" />
<meta
name="msapplication-config"
content="../assets/images/browserconfig.xml"
/>
<meta name="theme-color" content="#ffffff" />
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>Choices</title>
<meta name=description itemprop=description content="A lightweight, configurable select box/text input plugin. Similar to Select2 and Selectize but without the jQuery dependency.">
<link rel="apple-touch-icon" sizes="180x180" href="../assets/images/apple-touch-icon.png">
<link rel="icon" type="image/png" href="../assets/images/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="../assets/images/favicon-16x16.png" sizes="16x16">
<link rel="manifest" href="../assets/images/manifest.json">
<link rel="mask-icon" href="../assets/images/safari-pinned-tab.svg" color="#00bcd4">
<link rel="shortcut icon" href="../assets/images/favicon.ico">
<meta name="msapplication-config" content="../assets/images/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
<!-- Ignore these -->
<link rel="stylesheet" href="../assets/styles/base.min.css?version=6.0.3" />
<!-- End ignore these -->
<!-- Ignore these -->
<link rel="stylesheet" href="../assets/styles/base.min.css?version=6.0.3">
<!-- End ignore these -->
<!-- Choices includes -->
<link rel="stylesheet" href="../assets/styles/choices.min.css?version=6.0.3">
<script src="../assets/scripts/choices.min.js?version=6.0.3"></script>
<!-- End Choices includes -->
<!-- Choices includes -->
<link
rel="stylesheet"
href="../assets/styles/choices.min.css?version=6.0.3"
/>
<script src="../assets/scripts/choices.min.js?version=6.0.3"></script>
<!-- End Choices includes -->
</head>
<body>
@ -33,7 +63,12 @@
<label for="choices-basic">Basic</label>
<button class="disable push-bottom">Disable</button>
<button class="enable push-bottom">Enable</button>
<select class="form-control" name="choices-basic" id="choices-basic" multiple>
<select
class="form-control"
name="choices-basic"
id="choices-basic"
multiple
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Find me">Choice 3</option>
@ -43,7 +78,12 @@
<div data-test-hook="remove-button">
<label for="choices-remove-button">Remove button</label>
<select class="form-control" name="choices-remove-button" id="choices-remove-button" multiple>
<select
class="form-control"
name="choices-remove-button"
id="choices-remove-button"
multiple
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -53,7 +93,12 @@
<div data-test-hook="disabled-choice">
<label for="choices-disabled-choice">Disabled choice</label>
<select class="form-control" name="choices-disabled-choice" id="choices-disabled-choice" multiple>
<select
class="form-control"
name="choices-disabled-choice"
id="choices-disabled-choice"
multiple
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -63,7 +108,12 @@
<div data-test-hook="add-items-disabled">
<label for="choices-add-items-disabled">Add items disabled</label>
<select class="form-control" name="choices-add-items-disabled" id="choices-add-items-disabled" multiple>
<select
class="form-control"
name="choices-add-items-disabled"
id="choices-add-items-disabled"
multiple
>
<option value="Choice 1" selected>Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -72,7 +122,13 @@
<div data-test-hook="disabled-via-attr">
<label for="choices-disabled-via-attr">Disabled via attribute</label>
<select class="form-control" name="choices-disabled-via-attr" id="choices-disabled-via-attr" multiple disabled>
<select
class="form-control"
name="choices-disabled-via-attr"
id="choices-disabled-via-attr"
multiple
disabled
>
<option value="Choice 1" selected>Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -81,7 +137,12 @@
<div data-test-hook="selection-limit">
<label for="choices-selection-limit">Input limit</label>
<select class="form-control" name="choices-selection-limit" id="choices-selection-limit" multiple>
<select
class="form-control"
name="choices-selection-limit"
id="choices-selection-limit"
multiple
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -93,7 +154,12 @@
<div data-test-hook="prepend-append">
<label for="choices-prepend-append">Prepend/append</label>
<select class="form-control" name="choices-prepend-append" id="choices-prepend-append" multiple>
<select
class="form-control"
name="choices-prepend-append"
id="choices-prepend-append"
multiple
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -102,7 +168,12 @@
<div data-test-hook="render-choice-limit">
<label for="choices-render-choice-limit">Render choice limit</label>
<select class="form-control" name="choices-render-choice-limit" id="choices-render-choice-limit" multiple>
<select
class="form-control"
name="choices-render-choice-limit"
id="choices-render-choice-limit"
multiple
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -111,7 +182,12 @@
<div data-test-hook="search-floor">
<label for="choices-search-floor">Search floor</label>
<select class="form-control" name="choices-search-floor" id="choices-search-floor" multiple>
<select
class="form-control"
name="choices-search-floor"
id="choices-search-floor"
multiple
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -120,7 +196,12 @@
<div data-test-hook="placeholder">
<label for="choices-placeholder">Placeholder</label>
<select class="form-control" name="choices-placeholder" id="choices-placeholder" multiple>
<select
class="form-control"
name="choices-placeholder"
id="choices-placeholder"
multiple
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -129,12 +210,22 @@
<div data-test-hook="remote-data">
<label for="choices-remote-data">Remote data</label>
<select class="form-control" name="choices-remote-data" id="choices-remote-data" multiple></select>
<select
class="form-control"
name="choices-remote-data"
id="choices-remote-data"
multiple
></select>
</div>
<div data-test-hook="scrolling-dropdown">
<label for="choices-scrolling-dropdown">Scrolling dropdown</label>
<select class="form-control" name="choices-scrolling-dropdown" id="choices-scrolling-dropdown" multiple>
<select
class="form-control"
name="choices-scrolling-dropdown"
id="choices-scrolling-dropdown"
multiple
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -155,7 +246,12 @@
<div data-test-hook="groups">
<label for="choices-groups">Choice groups</label>
<select class="form-control" name="choices-groups" id="choices-groups" multiple>
<select
class="form-control"
name="choices-groups"
id="choices-groups"
multiple
>
<optgroup label="UK">
<option value="London">London</option>
<option value="Manchester">Manchester</option>
@ -171,26 +267,47 @@
<div data-test-hook="custom-properties">
<label for="choices-custom-properties">Custom properties</label>
<select class="form-control" name="choices-custom-properties" id="choices-custom-properties" multiple></select>
<select
class="form-control"
name="choices-custom-properties"
id="choices-custom-properties"
multiple
></select>
</div>
<div data-test-hook="non-string-values">
<label for="choices-non-string-values">Non-string values</label>
<select class="form-control" name="choices-non-string-values" id="choices-non-string-values"></select>
<select
class="form-control"
name="choices-non-string-values"
id="choices-non-string-values"
></select>
</div>
<div data-test-hook="within-form">
<form>
<label for="choices-within-form">Within form</label>
<select class="form-control" name="choices-within-form" id="choices-within-form" multiple>
<select
class="form-control"
name="choices-within-form"
id="choices-within-form"
multiple
>
<option value="Choice 1">Choice 1</option>
</select>
</form>
</div>
<div data-test-hook="set-choice-by-value">
<label for="choices-set-choice-by-value">Dynamically set choice by value</label>
<select class="form-control" name="choices-set-choice-by-value" id="choices-set-choice-by-value" multiple>
<label for="choices-set-choice-by-value"
>Dynamically set choice by value</label
>
<select
class="form-control"
name="choices-set-choice-by-value"
id="choices-set-choice-by-value"
multiple
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -199,7 +316,12 @@
<div data-test-hook="search-by-label">
<label for="choices-search-by-label">Search by label</label>
<select class="form-control" name="choices-search-by-label" id="choices-search-by-label" multiple>
<select
class="form-control"
name="choices-search-by-label"
id="choices-search-by-label"
multiple
>
<option value="value1">label1</option>
<option value="value2">label2</option>
</select>
@ -210,13 +332,17 @@
document.addEventListener('DOMContentLoaded', function() {
const choicesBasic = new Choices('#choices-basic');
document.querySelector('button.disable').addEventListener('click', () => {
choicesBasic.disable();
});
document
.querySelector('button.disable')
.addEventListener('click', () => {
choicesBasic.disable();
});
document.querySelector('button.enable').addEventListener('click', () => {
choicesBasic.enable();
});
document
.querySelector('button.enable')
.addEventListener('click', () => {
choicesBasic.enable();
});
new Choices('#choices-remove-button', {
removeItemButton: true,
@ -254,16 +380,9 @@
new Choices('#choices-remote-data', {
shouldSort: false,
}).ajax((callback) => {
fetch('/data')
.then((response) => {
response.json().then((data) => {
callback(data, 'value', 'label');
});
})
.catch((error) => {
console.error(error);
});
}).setChoices(async () => {
const data = await fetch('/data');
return data.json();
});
new Choices('#choices-scrolling-dropdown', {
@ -331,10 +450,12 @@
new Choices('#choices-within-form');
new Choices('#choices-set-choice-by-value').setChoiceByValue('Choice 2');
new Choices('#choices-set-choice-by-value').setChoiceByValue(
'Choice 2',
);
new Choices('#choices-search-by-label', { searchFields: ['label'] });
});
</script>
</body>
</html>
</html>

View file

@ -1,28 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width,initial-scale=1,user-scalable=no"
/>
<title>Choices</title>
<meta
name="description"
itemprop="description"
content="A lightweight, configurable select box/text input plugin. Similar to Select2 and Selectize but without the jQuery dependency."
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="../assets/images/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
href="../assets/images/favicon-32x32.png"
sizes="32x32"
/>
<link
rel="icon"
type="image/png"
href="../assets/images/favicon-16x16.png"
sizes="16x16"
/>
<link rel="manifest" href="../assets/images/manifest.json" />
<link
rel="mask-icon"
href="../assets/images/safari-pinned-tab.svg"
color="#00bcd4"
/>
<link rel="shortcut icon" href="../assets/images/favicon.ico" />
<meta
name="msapplication-config"
content="../assets/images/browserconfig.xml"
/>
<meta name="theme-color" content="#ffffff" />
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>Choices</title>
<meta name=description itemprop=description content="A lightweight, configurable select box/text input plugin. Similar to Select2 and Selectize but without the jQuery dependency.">
<link rel="apple-touch-icon" sizes="180x180" href="../assets/images/apple-touch-icon.png">
<link rel="icon" type="image/png" href="../assets/images/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="../assets/images/favicon-16x16.png" sizes="16x16">
<link rel="manifest" href="../assets/images/manifest.json">
<link rel="mask-icon" href="../assets/images/safari-pinned-tab.svg" color="#00bcd4">
<link rel="shortcut icon" href="../assets/images/favicon.ico">
<meta name="msapplication-config" content="../assets/images/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
<!-- Ignore these -->
<link rel="stylesheet" href="../assets/styles/base.min.css?version=6.0.3" />
<!-- End ignore these -->
<!-- Ignore these -->
<link rel="stylesheet" href="../assets/styles/base.min.css?version=6.0.3">
<!-- End ignore these -->
<!-- Choices includes -->
<link rel="stylesheet" href="../assets/styles/choices.min.css?version=6.0.3">
<script src="../assets/scripts/choices.min.js?version=6.0.3"></script>
<!-- End Choices includes -->
<!-- Choices includes -->
<link
rel="stylesheet"
href="../assets/styles/choices.min.css?version=6.0.3"
/>
<script src="../assets/scripts/choices.min.js?version=6.0.3"></script>
<!-- End Choices includes -->
</head>
<body>
@ -43,7 +73,11 @@
<div data-test-hook="remove-button">
<label for="choices-remove-button">Remove button</label>
<select class="form-control" name="choices-remove-button" id="choices-remove-button">
<select
class="form-control"
name="choices-remove-button"
id="choices-remove-button"
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -53,7 +87,11 @@
<div data-test-hook="disabled-choice">
<label for="choices-disabled-choice">Disabled choice</label>
<select class="form-control" name="choices-disabled-choice" id="choices-disabled-choice">
<select
class="form-control"
name="choices-disabled-choice"
id="choices-disabled-choice"
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -63,7 +101,11 @@
<div data-test-hook="add-items-disabled">
<label for="choices-add-items-disabled">Add items disabled</label>
<select class="form-control" name="choices-add-items-disabled" id="choices-add-items-disabled">
<select
class="form-control"
name="choices-add-items-disabled"
id="choices-add-items-disabled"
>
<option value="Choice 1" selected>Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -72,7 +114,12 @@
<div data-test-hook="disabled-via-attr">
<label for="choices-disabled-via-attr">Disabled via attribute</label>
<select class="form-control" name="choices-disabled-via-attr" id="choices-disabled-via-attr" disabled>
<select
class="form-control"
name="choices-disabled-via-attr"
id="choices-disabled-via-attr"
disabled
>
<option value="Choice 1" selected>Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -81,7 +128,11 @@
<div data-test-hook="prepend-append">
<label for="choices-prepend-append">Prepend/append</label>
<select class="form-control" name="choices-prepend-append" id="choices-prepend-append">
<select
class="form-control"
name="choices-prepend-append"
id="choices-prepend-append"
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -90,7 +141,11 @@
<div data-test-hook="render-choice-limit">
<label for="choices-render-choice-limit">Render choice limit</label>
<select class="form-control" name="choices-render-choice-limit" id="choices-render-choice-limit">
<select
class="form-control"
name="choices-render-choice-limit"
id="choices-render-choice-limit"
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -99,7 +154,11 @@
<div data-test-hook="search-disabled">
<label for="choices-search-disabled">Search disabled</label>
<select class="form-control" name="choices-search-disabled" id="choices-search-disabled">
<select
class="form-control"
name="choices-search-disabled"
id="choices-search-disabled"
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -108,7 +167,11 @@
<div data-test-hook="search-floor">
<label for="choices-search-floor">Search floor</label>
<select class="form-control" name="choices-search-floor" id="choices-search-floor">
<select
class="form-control"
name="choices-search-floor"
id="choices-search-floor"
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -117,12 +180,20 @@
<div data-test-hook="remote-data">
<label for="choices-remote-data">Remote data</label>
<select class="form-control" name="choices-remote-data" id="choices-remote-data"></select>
<select
class="form-control"
name="choices-remote-data"
id="choices-remote-data"
></select>
</div>
<div data-test-hook="scrolling-dropdown">
<label for="choices-scrolling-dropdown">Scrolling dropdown</label>
<select class="form-control" name="choices-scrolling-dropdown" id="choices-scrolling-dropdown">
<select
class="form-control"
name="choices-scrolling-dropdown"
id="choices-scrolling-dropdown"
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -143,7 +214,12 @@
<div data-test-hook="groups">
<label for="choices-groups">Choice groups</label>
<select class="form-control" name="choices-groups" id="choices-groups" multiple>
<select
class="form-control"
name="choices-groups"
id="choices-groups"
multiple
>
<optgroup label="UK">
<option value="London">London</option>
<option value="Manchester">Manchester</option>
@ -159,7 +235,11 @@
<div data-test-hook="parent-child">
<label for="choices-parent">Parent</label>
<select class="form-control" name="choices-parent" id="choices-parent">
<select
class="form-control"
name="choices-parent"
id="choices-parent"
>
<option value="Parent choice 1">Parent choice 1</option>
<option value="Parent choice 2">Parent choice 2</option>
<option value="Parent choice 3">Parent choice 3</option>
@ -175,26 +255,44 @@
<div data-test-hook="custom-properties">
<label for="choices-custom-properties">Custom properties</label>
<select class="form-control" name="choices-custom-properties" id="choices-custom-properties"></select>
<select
class="form-control"
name="choices-custom-properties"
id="choices-custom-properties"
></select>
</div>
<div data-test-hook="non-string-values">
<label for="choices-non-string-values">Non-string values</label>
<select class="form-control" name="choices-non-string-values" id="choices-non-string-values"></select>
<select
class="form-control"
name="choices-non-string-values"
id="choices-non-string-values"
></select>
</div>
<div data-test-hook="within-form">
<form>
<label for="choices-within-form">Within form</label>
<select class="form-control" name="choices-within-form" id="choices-within-form">
<select
class="form-control"
name="choices-within-form"
id="choices-within-form"
>
<option value="Choice 1">Choice 1</option>
</select>
</form>
</div>
<div data-test-hook="set-choice-by-value">
<label for="choices-set-choice-by-value">Dynamically set choice by value</label>
<select class="form-control" name="choices-set-choice-by-value" id="choices-set-choice-by-value">
<label for="choices-set-choice-by-value"
>Dynamically set choice by value</label
>
<select
class="form-control"
name="choices-set-choice-by-value"
id="choices-set-choice-by-value"
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -203,7 +301,11 @@
<div data-test-hook="search-by-label">
<label for="choices-search-by-label">Search by label</label>
<select class="form-control" name="choices-search-by-label" id="choices-search-by-label">
<select
class="form-control"
name="choices-search-by-label"
id="choices-search-by-label"
>
<option value="value1">label1</option>
<option value="value2">label2</option>
</select>
@ -225,13 +327,17 @@
document.addEventListener('DOMContentLoaded', function() {
const choicesBasic = new Choices('#choices-basic');
document.querySelector('button.disable').addEventListener('click', () => {
choicesBasic.disable();
});
document
.querySelector('button.disable')
.addEventListener('click', () => {
choicesBasic.disable();
});
document.querySelector('button.enable').addEventListener('click', () => {
choicesBasic.enable();
});
document
.querySelector('button.enable')
.addEventListener('click', () => {
choicesBasic.enable();
});
new Choices('#choices-remove-button', {
removeItemButton: true,
@ -253,12 +359,12 @@
});
new Choices('#choices-render-choice-limit', {
renderChoiceLimit: 1
renderChoiceLimit: 1,
});
new Choices('#choices-search-disabled', {
searchEnabled: false
})
searchEnabled: false,
});
new Choices('#choices-search-floor', {
searchFloor: 5,
@ -266,16 +372,9 @@
new Choices('#choices-remote-data', {
shouldSort: false,
}).ajax((callback) => {
fetch('/data')
.then((response) => {
response.json().then((data) => {
callback(data, 'value', 'label');
});
})
.catch((error) => {
console.error(error);
});
}).setChoices(async () => {
const res = await fetch('/data');
return res.json();
});
new Choices('#choices-scrolling-dropdown', {
@ -287,7 +386,7 @@
const parent = new Choices('#choices-parent');
const child = new Choices('#choices-child').disable();
parent.passedElement.element.addEventListener('change', (event) => {
parent.passedElement.element.addEventListener('change', event => {
if (event.detail.value === 'Parent choice 2') {
child.enable();
} else {
@ -321,8 +420,8 @@
customProperties: {
country: 'Portugal',
},
}
]
},
],
});
new Choices('#choices-non-string-values', {
@ -354,7 +453,9 @@
new Choices('#choices-within-form');
new Choices('#choices-set-choice-by-value').setChoiceByValue('Choice 2');
new Choices('#choices-set-choice-by-value').setChoiceByValue(
'Choice 2',
);
new Choices('#choices-search-by-label', { searchFields: ['label'] });
@ -368,4 +469,4 @@
});
</script>
</body>
</html>
</html>

View file

@ -1,28 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width,initial-scale=1,user-scalable=no"
/>
<title>Choices</title>
<meta
name="description"
itemprop="description"
content="A lightweight, configurable select box/text input plugin. Similar to Select2 and Selectize but without the jQuery dependency."
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="../assets/images/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
href="../assets/images/favicon-32x32.png"
sizes="32x32"
/>
<link
rel="icon"
type="image/png"
href="../assets/images/favicon-16x16.png"
sizes="16x16"
/>
<link rel="manifest" href="../assets/images/manifest.json" />
<link
rel="mask-icon"
href="../assets/images/safari-pinned-tab.svg"
color="#00bcd4"
/>
<link rel="shortcut icon" href="../assets/images/favicon.ico" />
<meta
name="msapplication-config"
content="../assets/images/browserconfig.xml"
/>
<meta name="theme-color" content="#ffffff" />
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>Choices</title>
<meta name=description itemprop=description content="A lightweight, configurable select box/text input plugin. Similar to Select2 and Selectize but without the jQuery dependency.">
<link rel="apple-touch-icon" sizes="180x180" href="../assets/images/apple-touch-icon.png">
<link rel="icon" type="image/png" href="../assets/images/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="../assets/images/favicon-16x16.png" sizes="16x16">
<link rel="manifest" href="../assets/images/manifest.json">
<link rel="mask-icon" href="../assets/images/safari-pinned-tab.svg" color="#00bcd4">
<link rel="shortcut icon" href="../assets/images/favicon.ico">
<meta name="msapplication-config" content="../assets/images/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
<!-- Ignore these -->
<link rel="stylesheet" href="../assets/styles/base.min.css?version=6.0.3" />
<!-- End ignore these -->
<!-- Ignore these -->
<link rel="stylesheet" href="../assets/styles/base.min.css?version=6.0.3">
<!-- End ignore these -->
<!-- Choices includes -->
<link rel="stylesheet" href="../assets/styles/choices.min.css?version=6.0.3">
<script src="../assets/scripts/choices.min.js?version=6.0.3"></script>
<!-- End Choices includes -->
<!-- Choices includes -->
<link
rel="stylesheet"
href="../assets/styles/choices.min.css?version=6.0.3"
/>
<script src="../assets/scripts/choices.min.js?version=6.0.3"></script>
<!-- End Choices includes -->
</head>
<body>
@ -31,63 +61,76 @@
<h2>Text inputs</h2>
<div data-test-hook="basic">
<label for="choices-basic">Basic</label>
<input class="form-control" id="choices-basic" type="text">
<input class="form-control" id="choices-basic" type="text" />
</div>
<div data-test-hook="edit-items">
<label for="choices-edit-items">Edit items</label>
<input class="form-control" id="choices-edit-items" type="text">
<input class="form-control" id="choices-edit-items" type="text" />
</div>
<div data-test-hook="remove-button">
<label for="choices-remove-button">Remove button</label>
<input class="form-control" id="choices-remove-button" type="text">
<input class="form-control" id="choices-remove-button" type="text" />
</div>
<div data-test-hook="unique-values">
<label for="choices-unique-values">Unique values</label>
<input class="form-control" id="choices-unique-values" type="text">
<input class="form-control" id="choices-unique-values" type="text" />
</div>
<div data-test-hook="input-limit">
<label for="choices-input-limit">Input limit</label>
<input class="form-control" id="choices-input-limit" type="text">
<input class="form-control" id="choices-input-limit" type="text" />
</div>
<div data-test-hook="add-item-filter">
<label for="choices-add-item-filter">Add item filter</label>
<input class="form-control" id="choices-add-item-filter" type="text">
<input
class="form-control"
id="choices-add-item-filter"
type="text"
/>
</div>
<div data-test-hook="adding-items-disabled">
<label for="choices-adding-items-disabled">Add items disabled</label>
<input class="form-control" id="choices-adding-items-disabled" type="text">
<input
class="form-control"
id="choices-adding-items-disabled"
type="text"
/>
</div>
<div data-test-hook="disabled-via-attr">
<label for="choices-disabled-via-attr">Disabled via attribute</label>
<input class="form-control" id="choices-disabled-via-attr" type="text" disabled>
<input
class="form-control"
id="choices-disabled-via-attr"
type="text"
disabled
/>
</div>
<div data-test-hook="prepend-append">
<label for="choices-prepend-append">Prepend/append</label>
<input class="form-control" id="choices-prepend-append" type="text">
<input class="form-control" id="choices-prepend-append" type="text" />
</div>
<div data-test-hook="prepopulated">
<label for="choices-prepopulated">Pre-populated choices</label>
<input class="form-control" id="choices-prepopulated" type="text">
<input class="form-control" id="choices-prepopulated" type="text" />
</div>
<div data-test-hook="placeholder">
<label for="choices-placeholder">Placeholder</label>
<input class="form-control" id="choices-placeholder" type="text">
<input class="form-control" id="choices-placeholder" type="text" />
</div>
<div data-test-hook="within-form">
<form>
<label for="choices-within-form">Within form</label>
<input class="form-control" id="choices-within-form" type="text">
<input class="form-control" id="choices-within-form" type="text" />
</form>
</div>
</div>
@ -114,8 +157,8 @@
new Choices('#choices-add-item-filter', {
addItems: true,
addItemFilterFn: (value) => {
const regex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
addItemFilter: value => {
const regex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
const expression = new RegExp(regex.source, 'i');
return expression.test(value);
},
@ -133,13 +176,16 @@
});
new Choices('#choices-prepopulated', {
items: ['Josh Johnson', {
value: 'joe@bloggs.co.uk',
label: 'Joe Bloggs',
customProperties: {
description: 'Joe Blogg is such a generic name',
}
}],
items: [
'Josh Johnson',
{
value: 'joe@bloggs.co.uk',
label: 'Joe Bloggs',
customProperties: {
description: 'Joe Blogg is such a generic name',
},
},
],
});
new Choices('#choices-placeholder', {
@ -151,4 +197,4 @@
});
</script>
</body>
</html>
</html>

View file

@ -1,4 +1,4 @@
import { ACTION_TYPES } from './../constants';
import { ACTION_TYPES } from '../constants';
export const addChoice = ({
value,

View file

@ -1,5 +1,3 @@
/* eslint-disable import/prefer-default-export */
export const setIsLoading = isLoading => ({
type: 'SET_IS_LOADING',
isLoading,

View file

@ -1,6 +1,5 @@
import { ACTION_TYPES } from './../constants';
import { ACTION_TYPES } from '../constants';
/* eslint-disable import/prefer-default-export */
export const addGroup = (value, id, active, disabled) => ({
type: ACTION_TYPES.ADD_GROUP,
value,

View file

@ -1,4 +1,4 @@
import { ACTION_TYPES } from './../constants';
import { ACTION_TYPES } from '../constants';
export const addItem = ({
value,

File diff suppressed because it is too large Load diff

View file

@ -2,40 +2,222 @@ import { expect } from 'chai';
import { spy, stub } from 'sinon';
import Choices from './choices';
import { EVENTS, ACTION_TYPES } from './constants';
import { EVENTS, ACTION_TYPES, DEFAULT_CONFIG } from './constants';
import { WrappedSelect, WrappedInput } from './components/index';
describe('choices', () => {
let instance;
let output;
let passedElement;
beforeEach(() => {
passedElement = document.createElement('input');
passedElement.type = 'text';
passedElement.className = 'js-choices';
document.body.appendChild(passedElement);
instance = new Choices(passedElement);
});
afterEach(() => {
output = null;
instance = null;
});
const returnsInstance = () => {
it('returns this', () => {
expect(output).to.eql(instance);
});
};
describe('constructor', () => {
describe('config', () => {
describe('not passing config options', () => {
it('uses the default config', () => {
document.body.innerHTML = `
<input data-choice type="text" id="input-1" />
`;
instance = new Choices();
expect(instance.config).to.eql(DEFAULT_CONFIG);
});
});
describe('passing config options', () => {
it('merges the passed config with the default config', () => {
document.body.innerHTML = `
<input data-choice type="text" id="input-1" />
`;
const config = {
renderChoiceLimit: 5,
};
instance = new Choices('[data-choice]', config);
expect(instance.config).to.eql({
...DEFAULT_CONFIG,
...config,
});
});
});
});
describe('not passing an element', () => {
it('returns a Choices instance for the first element with a "data-choice" attribute', () => {
document.body.innerHTML = `
<input data-choice type="text" id="input-1" />
<input data-choice type="text" id="input-2" />
<input data-choice type="text" id="input-3" />
`;
const inputs = document.querySelectorAll('[data-choice]');
expect(inputs.length).to.equal(3);
instance = new Choices();
expect(instance.passedElement.element.id).to.equal(inputs[0].id);
});
describe('when an element cannot be found in the DOM', () => {
it('throws an error', () => {
document.body.innerHTML = ``;
expect(() => new Choices()).to.throw(
TypeError,
'Expected one of the following types text|select-one|select-multiple',
);
});
});
});
describe('passing an element', () => {
describe('passing an element that has not been initialised with Choices', () => {
beforeEach(() => {
document.body.innerHTML = `
<input type="text" id="input-1" />
`;
});
it('sets the initialised flag to true', () => {
instance = new Choices('#input-1');
expect(instance.initialised).to.equal(true);
});
it('intialises', () => {
const initSpy = spy();
// initialise with the same element
instance = new Choices('#input-1', {
silent: true,
callbackOnInit: initSpy,
});
expect(initSpy.called).to.equal(true);
});
});
describe('passing an element that has already be initialised with Choices', () => {
beforeEach(() => {
document.body.innerHTML = `
<input type="text" id="input-1" />
`;
// initialise once
new Choices('#input-1', { silent: true });
});
it('sets the initialised flag to true', () => {
// initialise with the same element
instance = new Choices('#input-1', { silent: true });
expect(instance.initialised).to.equal(true);
});
it('does not reinitialise', () => {
const initSpy = spy();
// initialise with the same element
instance = new Choices('#input-1', {
silent: true,
callbackOnInit: initSpy,
});
expect(initSpy.called).to.equal(false);
});
});
describe(`passing an element as a DOMString`, () => {
describe('passing a input element type', () => {
it('sets the "passedElement" instance property as an instance of WrappedInput', () => {
document.body.innerHTML = `
<input data-choice type="text" id="input-1" />
`;
instance = new Choices('[data-choice]');
expect(instance.passedElement).to.be.an.instanceOf(WrappedInput);
});
});
describe('passing a select element type', () => {
it('sets the "passedElement" instance property as an instance of WrappedSelect', () => {
document.body.innerHTML = `
<select data-choice id="select-1"></select>
`;
instance = new Choices('[data-choice]');
expect(instance.passedElement).to.be.an.instanceOf(WrappedSelect);
});
});
});
describe(`passing an element as a HTMLElement`, () => {
describe('passing a input element type', () => {
it('sets the "passedElement" instance property as an instance of WrappedInput', () => {
document.body.innerHTML = `
<input data-choice type="text" id="input-1" />
`;
instance = new Choices(document.querySelector('[data-choice]'));
expect(instance.passedElement).to.be.an.instanceOf(WrappedInput);
});
});
describe('passing a select element type', () => {
it('sets the "passedElement" instance property as an instance of WrappedSelect', () => {
document.body.innerHTML = `
<select data-choice id="select-1"></select>
`;
instance = new Choices(document.querySelector('[data-choice]'));
expect(instance.passedElement).to.be.an.instanceOf(WrappedSelect);
});
});
});
describe('passing an invalid element type', () => {
it('throws an TypeError', () => {
document.body.innerHTML = `
<div data-choice id="div-1"></div>
`;
expect(() => new Choices('[data-choice]')).to.throw(
TypeError,
'Expected one of the following types text|select-one|select-multiple',
);
});
});
});
});
describe('public methods', () => {
beforeEach(() => {
passedElement = document.createElement('input');
passedElement.type = 'text';
passedElement.className = 'js-choices';
document.body.appendChild(passedElement);
instance = new Choices(passedElement);
});
afterEach(() => {
output = null;
instance = null;
});
describe('init', () => {
const callbackOnInitSpy = spy();
beforeEach(() => {
instance = new Choices(passedElement, {
callbackOnInit: callbackOnInitSpy,
silent: true,
});
});
@ -171,7 +353,7 @@ describe('choices', () => {
});
it('nullifys templates config', () => {
expect(instance.config.templates).to.equal(null);
expect(instance._templates).to.equal(null);
});
it('resets initialise flag', () => {
@ -797,6 +979,29 @@ describe('choices', () => {
});
});
describe('clearChoices', () => {
let storeDispatchStub;
beforeEach(() => {
storeDispatchStub = stub();
instance._store.dispatch = storeDispatchStub;
output = instance.clearChoices();
});
afterEach(() => {
instance._store.dispatch.reset();
});
returnsInstance(output);
it('dispatches clearChoices action', () => {
expect(storeDispatchStub.lastCall.args[0]).to.eql({
type: ACTION_TYPES.CLEAR_CHOICES,
});
});
});
describe('clearStore', () => {
let storeDispatchStub;
@ -880,84 +1085,75 @@ describe('choices', () => {
});
});
describe('ajax', () => {
const callbackoutput = 'worked';
let handleLoadingStateStub;
let ajaxCallbackStub;
const returnsEarly = () => {
it('returns early', () => {
expect(handleLoadingStateStub.called).to.equal(false);
expect(ajaxCallbackStub.called).to.equal(false);
});
};
beforeEach(() => {
handleLoadingStateStub = stub();
ajaxCallbackStub = stub().returns(callbackoutput);
instance._ajaxCallback = ajaxCallbackStub;
instance._handleLoadingState = handleLoadingStateStub;
});
afterEach(() => {
instance._ajaxCallback.reset();
instance._handleLoadingState.reset();
});
describe('setChoices with callback/Promise', () => {
describe('not initialised', () => {
beforeEach(() => {
instance.initialised = false;
output = instance.ajax(() => {});
});
returnsInstance(output);
returnsEarly();
it('should throw', () => {
expect(() => instance.setChoices(null)).Throw(ReferenceError);
});
});
describe('text element', () => {
beforeEach(() => {
instance._isSelectElement = false;
output = instance.ajax(() => {});
});
returnsInstance(output);
returnsEarly();
it('should throw', () => {
expect(() => instance.setChoices(null)).Throw(TypeError);
});
});
describe('passing invalid function', () => {
beforeEach(() => {
output = instance.ajax(null);
instance._isSelectElement = true;
});
returnsInstance(output);
returnsEarly();
it('should throw on non function', () => {
expect(() => instance.setChoices(null)).Throw(TypeError, /Promise/i);
});
it(`should throw on function that doesn't return promise`, () => {
expect(() => instance.setChoices(() => 'boo')).to.throw(
TypeError,
/promise/i,
);
});
});
describe('select element', () => {
let callback;
it('fetches and sets choices', async () => {
document.body.innerHTML = '<select id="test" />';
const choice = new Choices('#test');
const handleLoadingStateSpy = spy(choice, '_handleLoadingState');
beforeEach(() => {
instance.initialised = true;
instance._isSelectElement = true;
ajaxCallbackStub = stub();
callback = stub();
output = instance.ajax(callback);
});
let fetcherCalled = false;
const fetcher = async inst => {
expect(inst).to.eq(choice);
fetcherCalled = true;
await new Promise(resolve => setTimeout(resolve, 1000));
returnsInstance(output);
it('sets loading state', done => {
requestAnimationFrame(() => {
expect(handleLoadingStateStub.called).to.equal(true);
done();
});
});
it('calls passed function with ajax callback', () => {
expect(callback.called).to.equal(true);
expect(callback.lastCall.args[0]).to.eql(callbackoutput);
return [
{ label: 'l1', value: 'v1', customProperties: 'prop1' },
{ label: 'l2', value: 'v2', customProperties: 'prop2' },
];
};
expect(choice._store.choices.length).to.equal(0);
const promise = choice.setChoices(fetcher);
await new Promise(resolve =>
requestAnimationFrame(() => {
expect(handleLoadingStateSpy.callCount).to.equal(1);
resolve();
}),
);
expect(fetcherCalled).to.be.true;
const res = await promise;
expect(res).to.equal(choice);
expect(choice._store.choices[1].value).to.equal('v2');
expect(choice._store.choices[1].label).to.equal('l2');
expect(choice._store.choices[1].customProperties).to.equal('prop2');
});
});
});
@ -1330,7 +1526,7 @@ describe('choices', () => {
...choices[0],
choices,
},
...choices[1],
choices[1],
];
beforeEach(() => {
@ -1352,31 +1548,29 @@ describe('choices', () => {
instance.containerOuter.removeLoadingState.reset();
});
const returnsEarly = () => {
it('returns early', () => {
expect(addGroupStub.called).to.equal(false);
expect(addChoiceStub.called).to.equal(false);
expect(clearChoicesStub.called).to.equal(false);
});
};
describe('when element is not select element', () => {
beforeEach(() => {
instance._isSelectElement = false;
instance.setChoices(choices, value, label, false);
});
returnsEarly();
it('throws', () => {
expect(() =>
instance.setChoices(choices, value, label, false),
).to.throw(TypeError, /input/i);
});
});
describe('passing invalid arguments', () => {
describe('passing no value', () => {
beforeEach(() => {
instance._isSelectElement = true;
instance.setChoices(choices, undefined, 'label', false);
});
returnsEarly();
it('throws', () => {
expect(() =>
instance.setChoices(choices, null, 'label', false),
).to.throw(TypeError, /value/i);
});
});
});
@ -1451,7 +1645,9 @@ describe('choices', () => {
});
});
});
});
describe('private methods', () => {
describe('_createGroupsFragment', () => {
let _createChoicesFragmentStub;
const choices = [

View file

@ -1,4 +1,4 @@
import { getWindowHeight, wrap } from '../lib/utils';
import { wrap } from '../lib/utils';
export default class Container {
constructor({ element, type, classNames, position }) {
@ -38,8 +38,8 @@ export default class Container {
* @param {Number} dropdownPos
* @returns
*/
shouldFlip(dropdownPos, windowHeight = getWindowHeight()) {
if (dropdownPos === undefined) {
shouldFlip(dropdownPos) {
if (typeof dropdownPos !== 'number') {
return false;
}
@ -47,7 +47,8 @@ export default class Container {
// greater than the window height flip the dropdown.
let shouldFlip = false;
if (this.position === 'auto') {
shouldFlip = dropdownPos >= windowHeight;
shouldFlip = !window.matchMedia(`(min-height: ${dropdownPos + 1}px)`)
.matches;
} else if (this.position === 'top') {
shouldFlip = true;
}

View file

@ -102,18 +102,6 @@ describe('components/container', () => {
beforeEach(() => {
instance.position = 'auto';
});
describe('dropdownPos is greater than window height', () => {
it('returns false', () => {
expect(instance.shouldFlip(100, 1000)).to.equal(false);
});
});
describe('dropdownPos is less than window height', () => {
it('returns true', () => {
expect(instance.shouldFlip(100, 50)).to.equal(true);
});
});
});
describe('position config option set to "top"', () => {

View file

@ -6,16 +6,11 @@ export default class Dropdown {
}
/**
* Determine how far the top of our element is from
* the top of the window
* @return {Number} Vertical position
* Bottom position of dropdown in viewport coordinates
* @type {number} Vertical position
*/
distanceFromTopWindow() {
this.dimensions = this.element.getBoundingClientRect();
this.position = Math.ceil(
this.dimensions.top + window.pageYOffset + this.element.offsetHeight,
);
return this.position;
get distanceFromTopWindow() {
return this.element.getBoundingClientRect().bottom;
}
/**
@ -35,6 +30,7 @@ export default class Dropdown {
this.element.classList.add(this.classNames.activeState);
this.element.setAttribute('aria-expanded', 'true');
this.isActive = true;
return this;
}
@ -47,6 +43,7 @@ export default class Dropdown {
this.element.classList.remove(this.classNames.activeState);
this.element.setAttribute('aria-expanded', 'false');
this.isActive = false;
return this;
}
}

View file

@ -34,15 +34,13 @@ describe('components/dropdown', () => {
describe('distanceFromTopWindow', () => {
let top;
let offset;
let dimensions;
let getBoundingClientRectStub;
beforeEach(() => {
top = 100;
offset = 50;
dimensions = {
bottom: 0,
bottom: 121,
height: 0,
left: 0,
right: 0,
@ -53,31 +51,17 @@ describe('components/dropdown', () => {
getBoundingClientRectStub = sinon
.stub(instance.element, 'getBoundingClientRect')
.returns(dimensions);
window.pageYOffset = 50;
});
afterEach(() => {
getBoundingClientRectStub.restore();
});
it('determines how far the top of our element is from the top of the window', () => {
const expectedResponse = top + offset;
const actualResponse = instance.distanceFromTopWindow();
it('determines how far the top of our element is from the top of the viewport', () => {
const expectedResponse = dimensions.bottom;
const actualResponse = instance.distanceFromTopWindow;
expect(actualResponse).to.equal(expectedResponse);
});
it('assigns dimensions to instance', () => {
instance.distanceFromTopWindow();
const expectedResponse = dimensions;
expect(instance.dimensions).to.equal(expectedResponse);
});
it('assigns posisiton to instance', () => {
instance.distanceFromTopWindow();
const expectedResponse = top + offset;
expect(instance.position).to.equal(expectedResponse);
});
});
describe('getChild', () => {

View file

@ -1,12 +1,20 @@
import { calcWidthOfInput, sanitise } from '../lib/utils';
import { sanitise } from '../lib/utils';
export default class Input {
constructor({ element, type, classNames, placeholderValue }) {
Object.assign(this, { element, type, classNames, placeholderValue });
/**
*
* @typedef {import('../../../types/index').Choices.passedElement} passedElement
* @typedef {import('../../../types/index').Choices.ClassNames} ClassNames
* @param {{element: HTMLInputElement, type: passedElement['type'], classNames: ClassNames, preventPaste: boolean }} p
*/
constructor({ element, type, classNames, preventPaste }) {
this.element = element;
this.type = type;
this.classNames = classNames;
this.preventPaste = preventPaste;
this.isFocussed = this.element === document.activeElement;
this.isDisabled = false;
this.isDisabled = element.disabled;
this._onPaste = this._onPaste.bind(this);
this._onInput = this._onInput.bind(this);
this._onFocus = this._onFocus.bind(this);
@ -17,34 +25,38 @@ export default class Input {
this.element.placeholder = placeholder;
}
set value(value) {
this.element.value = value;
}
get value() {
return sanitise(this.element.value);
}
addEventListeners() {
this.element.addEventListener('input', this._onInput);
this.element.addEventListener('paste', this._onPaste);
this.element.addEventListener('focus', this._onFocus);
this.element.addEventListener('blur', this._onBlur);
set value(value) {
this.element.value = value;
}
if (this.element.form) {
this.element.form.addEventListener('reset', this._onFormReset);
}
addEventListeners() {
this.element.addEventListener('paste', this._onPaste);
this.element.addEventListener('input', this._onInput, {
passive: true,
});
this.element.addEventListener('focus', this._onFocus, {
passive: true,
});
this.element.addEventListener('blur', this._onBlur, {
passive: true,
});
}
removeEventListeners() {
this.element.removeEventListener('input', this._onInput);
this.element.removeEventListener('input', this._onInput, {
passive: true,
});
this.element.removeEventListener('paste', this._onPaste);
this.element.removeEventListener('focus', this._onFocus);
this.element.removeEventListener('blur', this._onBlur);
if (this.element.form) {
this.element.form.removeEventListener('reset', this._onFormReset);
}
this.element.removeEventListener('focus', this._onFocus, {
passive: true,
});
this.element.removeEventListener('blur', this._onBlur, {
passive: true,
});
}
enable() {
@ -89,30 +101,12 @@ export default class Input {
/**
* Set the correct input width based on placeholder
* value or input value
* @return
*/
setWidth(enforceWidth) {
const callback = width => {
this.element.style.width = width;
};
if (this._placeholderValue) {
// If there is a placeholder, we only want to set the width of the input when it is a greater
// length than 75% of the placeholder. This stops the input jumping around.
const valueHasDesiredLength =
this.element.value.length >= this._placeholderValue.length / 1.25;
if ((this.element.value && valueHasDesiredLength) || enforceWidth) {
this.calcWidth(callback);
}
} else {
// If there is no placeholder, resize input to contents
this.calcWidth(callback);
}
}
calcWidth(callback) {
return calcWidthOfInput(this.element, callback);
setWidth() {
// Resize input to contents or placeholder
const { style, value, placeholder } = this.element;
style.minWidth = `${placeholder.length + 1}ch`;
style.width = `${value.length + 1}ch`;
}
setActiveDescendant(activeDescendantID) {
@ -130,8 +124,7 @@ export default class Input {
}
_onPaste(event) {
const { target } = event;
if (target === this.element && this.preventPaste) {
if (this.preventPaste) {
event.preventDefault();
}
}

View file

@ -46,11 +46,12 @@ describe('components/input', () => {
it('adds event listeners', () => {
instance.addEventListeners();
expect(addEventListenerStub.callCount).to.equal(4);
expect(addEventListenerStub.getCall(0).args[0]).to.equal('input');
expect(addEventListenerStub.getCall(1).args[0]).to.equal('paste');
expect(addEventListenerStub.getCall(2).args[0]).to.equal('focus');
expect(addEventListenerStub.getCall(3).args[0]).to.equal('blur');
expect(['input', 'paste', 'focus', 'blur']).to.have.members(
Array.from(
{ length: addEventListenerStub.callCount },
(v, i) => addEventListenerStub.getCall(i).args[0],
),
);
});
});
@ -264,61 +265,21 @@ describe('components/input', () => {
});
});
/**
* Blocked by lack of ch support in JSDOM
* @see {@link https://github.com/jsdom/cssstyle/pull/107}
*
describe('setWidth', () => {
let calcWidthStub;
const inputWidth = '200px';
beforeEach(() => {
calcWidthStub = stub(instance, 'calcWidth').callsArgWith(0, inputWidth);
});
afterEach(() => {
calcWidthStub.restore();
});
describe('with a placeholder', () => {
describe('when value length is greater or equal to 75% of the placeholder length', () => {
it('sets the width of the element based on input value', () => {
instance._placeholderValue = 'This is a test';
instance.element.value = 'This is a test';
expect(instance.element.style.width).to.not.equal(inputWidth);
instance.setWidth();
expect(calcWidthStub.callCount).to.equal(1);
expect(instance.element.style.width).to.equal(inputWidth);
});
});
describe('when width is enforced', () => {
it('sets the width of the element based on input value', () => {
instance._placeholderValue = 'This is a test';
instance.element.value = '';
expect(instance.element.style.width).to.not.equal(inputWidth);
instance.setWidth(true);
expect(calcWidthStub.callCount).to.equal(1);
expect(instance.element.style.width).to.equal(inputWidth);
});
});
describe('when value length is less than 75% of the placeholder length', () => {
it('does not set the width of the element', () => {
instance._placeholderValue = 'This is a test';
instance.element.value = 'Test';
instance.setWidth();
expect(calcWidthStub.callCount).to.equal(0);
});
});
});
describe('without a placeholder', () => {
it('sets the width of the element based on input value', () => {
instance.placeholder = null;
expect(instance.element.style.width).to.not.equal(inputWidth);
instance.setWidth();
expect(calcWidthStub.callCount).to.equal(1);
expect(instance.element.style.width).to.equal(inputWidth);
});
it('sets the width of the element based on input value and placeholder', () => {
instance.placeholder = 'This is a placeholder';
instance.element.value = 'This is a value';
expect(instance.element.style.width).to.not.equal('16ch');
instance.setWidth();
expect(instance.element.style.width).to.equal('16ch');
expect(instance.element.style.minWidth).to.equal('22ch');
});
});
*/
describe('placeholder setter', () => {
it('sets value of element to passed placeholder', () => {

View file

@ -6,7 +6,6 @@ export default class List {
this.scrollPos = this.element.scrollTop;
this.height = this.element.offsetHeight;
this.hasChildren = !!this.element.children;
}
clear() {
@ -21,6 +20,10 @@ export default class List {
return this.element.querySelector(selector);
}
hasChildren() {
return this.element.hasChildNodes();
}
scrollToTop() {
this.element.scrollTop = 0;
}
@ -37,52 +40,52 @@ export default class List {
// Scroll position of dropdown
const containerScrollPos = this.element.scrollTop + dropdownHeight;
// Difference between the choice and scroll position
const endpoint =
const destination =
direction > 0
? this.element.scrollTop + choicePos - containerScrollPos
: choice.offsetTop;
requestAnimationFrame(time => {
this._animateScroll(time, endpoint, direction);
this._animateScroll(time, destination, direction);
});
}
_scrollDown(scrollPos, strength, endpoint) {
const easing = (endpoint - scrollPos) / strength;
_scrollDown(scrollPos, strength, destination) {
const easing = (destination - scrollPos) / strength;
const distance = easing > 1 ? easing : 1;
this.element.scrollTop = scrollPos + distance;
}
_scrollUp(scrollPos, strength, endpoint) {
const easing = (scrollPos - endpoint) / strength;
_scrollUp(scrollPos, strength, destination) {
const easing = (scrollPos - destination) / strength;
const distance = easing > 1 ? easing : 1;
this.element.scrollTop = scrollPos - distance;
}
_animateScroll(time, endpoint, direction) {
_animateScroll(time, destination, direction) {
const strength = SCROLLING_SPEED;
const choiceListScrollTop = this.element.scrollTop;
let continueAnimation = false;
if (direction > 0) {
this._scrollDown(choiceListScrollTop, strength, endpoint);
this._scrollDown(choiceListScrollTop, strength, destination);
if (choiceListScrollTop < endpoint) {
if (choiceListScrollTop < destination) {
continueAnimation = true;
}
} else {
this._scrollUp(choiceListScrollTop, strength, endpoint);
this._scrollUp(choiceListScrollTop, strength, destination);
if (choiceListScrollTop > endpoint) {
if (choiceListScrollTop > destination) {
continueAnimation = true;
}
}
if (continueAnimation) {
requestAnimationFrame(() => {
this._animateScroll(time, endpoint, direction);
this._animateScroll(time, destination, direction);
});
}
}

View file

@ -21,6 +21,10 @@ describe('components/list', () => {
it('assigns choices element to class', () => {
expect(instance.element).to.eql(choicesElement);
});
it('sets the height of the element', () => {
expect(instance.height).to.eql(choicesElement.scrollTop);
});
});
describe('clear', () => {
@ -62,4 +66,31 @@ describe('components/list', () => {
expect(expectedResponse).to.eql(actualResponse);
});
});
describe('hasChildren', () => {
describe('when list has children', () => {
it('returns true', () => {
const childElement = document.createElement('span');
instance.element.appendChild(childElement);
const response = instance.hasChildren();
expect(response).to.equal(true);
});
});
describe('when list does not have children', () => {
it('returns false', () => {
instance.element.innerHTML = '';
const response = instance.hasChildren();
expect(response).to.equal(false);
});
});
});
describe('scrollToTop', () => {
it("sets the position's scroll position to 0", () => {
instance.element.scrollTop = 10;
instance.scrollToTop();
expect(instance.element.scrollTop).to.equal(0);
});
});
});

View file

@ -1,10 +1,10 @@
import { dispatchEvent, isElement } from '../lib/utils';
import { dispatchEvent } from '../lib/utils';
export default class WrappedElement {
constructor({ element, classNames }) {
Object.assign(this, { element, classNames });
if (!isElement(element)) {
if (!(element instanceof Element)) {
throw new TypeError('Invalid element passed');
}
@ -15,10 +15,15 @@ export default class WrappedElement {
return this.element.value;
}
set value(value) {
// you must define setter here otherwise it will be readonly property
this.element.value = value;
}
conceal() {
// Hide passed input
this.element.classList.add(this.classNames.input);
this.element.classList.add(this.classNames.hiddenState);
this.element.hidden = true;
// Remove element from tab index
this.element.tabIndex = '-1';
@ -30,14 +35,13 @@ export default class WrappedElement {
this.element.setAttribute('data-choice-orig-style', origStyle);
}
this.element.setAttribute('aria-hidden', 'true');
this.element.setAttribute('data-choice', 'active');
}
reveal() {
// Reinstate passed element
this.element.classList.remove(this.classNames.input);
this.element.classList.remove(this.classNames.hiddenState);
this.element.hidden = false;
this.element.removeAttribute('tabindex');
// Recover original styles if any
@ -49,11 +53,11 @@ export default class WrappedElement {
} else {
this.element.removeAttribute('style');
}
this.element.removeAttribute('aria-hidden');
this.element.removeAttribute('data-choice');
// Re-assign values - this is weird, I know
this.element.value = this.element.value;
// @todo Figure out why we need to do this
this.element.value = this.element.value; // eslint-disable-line no-self-assign
}
enable() {

View file

@ -53,10 +53,7 @@ describe('components/wrappedElement', () => {
expect(
instance.element.classList.contains(instance.classNames.input),
).to.equal(true);
expect(
instance.element.classList.contains(instance.classNames.hiddenState),
).to.equal(true);
expect(instance.element.getAttribute('aria-hidden')).to.equal('true');
expect(instance.element.hidden).to.be.true;
expect(instance.element.getAttribute('data-choice')).to.equal('active');
expect(instance.element.getAttribute('data-choice-orig-style')).to.equal(
originalStyling,
@ -78,9 +75,7 @@ describe('components/wrappedElement', () => {
expect(
instance.element.classList.contains(instance.classNames.input),
).to.equal(false);
expect(
instance.element.classList.contains(instance.classNames.hiddenState),
).to.equal(false);
expect(instance.element.hidden).to.be.false;
expect(instance.element.getAttribute('style')).to.equal(originalStyling);
expect(instance.element.getAttribute('aria-hidden')).to.equal(null);
expect(instance.element.getAttribute('data-choice')).to.equal(null);

View file

@ -6,6 +6,10 @@ export default class WrappedInput extends WrappedElement {
this.delimiter = delimiter;
}
get value() {
return this.element.value;
}
set value(items) {
const itemValues = items.map(({ value }) => value);
const joinedValues = itemValues.join(this.delimiter);
@ -13,9 +17,4 @@ export default class WrappedInput extends WrappedElement {
this.element.setAttribute('value', joinedValues);
this.element.value = joinedValues;
}
// @todo figure out why we need this? Perhaps a babel issue
get value() {
return super.value;
}
}

View file

@ -1,13 +1,17 @@
import WrappedElement from './wrapped-element';
import templates from './../templates';
export default class WrappedSelect extends WrappedElement {
constructor({ element, classNames }) {
constructor({ element, classNames, template }) {
super({ element, classNames });
this.template = template;
}
get placeholderOption() {
return this.element.querySelector('option[placeholder]');
return (
this.element.querySelector('option[value=""]') ||
// Backward compatibility layer for the non-standard placeholder attribute supported in older versions.
this.element.querySelector('option[placeholder]')
);
}
get optionGroups() {
@ -22,9 +26,9 @@ export default class WrappedSelect extends WrappedElement {
const fragment = document.createDocumentFragment();
const addOptionToFragment = data => {
// Create a standard select option
const template = templates.option(data);
const option = this.template(data);
// Append it to fragment
fragment.appendChild(template);
fragment.appendChild(option);
};
// Add each list item to list

View file

@ -1,8 +1,9 @@
import { expect } from 'chai';
import { stub } from 'sinon';
import { stub, spy } from 'sinon';
import WrappedElement from './wrapped-element';
import WrappedSelect from './wrapped-select';
import { DEFAULT_CLASSNAMES } from '../constants';
import Templates from '../templates';
describe('components/wrappedSelect', () => {
let instance;
@ -11,11 +12,16 @@ describe('components/wrappedSelect', () => {
beforeEach(() => {
element = document.createElement('select');
element.id = 'target';
for (let i = 1; i <= 4; i++) {
for (let i = 0; i <= 4; i++) {
const option = document.createElement('option');
option.value = `Value ${i}`;
option.innerHTML = `Label ${i}`;
if (i === 0) {
option.value = '';
option.innerHTML = 'Placeholder label';
} else {
option.value = `Value ${i}`;
option.innerHTML = `Label ${i}`;
}
if (i === 1) {
option.setAttribute('placeholder', '');
@ -28,6 +34,7 @@ describe('components/wrappedSelect', () => {
instance = new WrappedSelect({
element: document.getElementById('target'),
classNames: DEFAULT_CLASSNAMES,
template: spy(Templates.option),
});
});
@ -67,8 +74,16 @@ describe('components/wrappedSelect', () => {
});
describe('placeholderOption getter', () => {
it('returns option element with placeholder attribute', () => {
it('returns option element with empty value attribute', () => {
expect(instance.placeholderOption).to.be.instanceOf(HTMLOptionElement);
expect(instance.placeholderOption.value).to.equal('');
});
it('returns option element with placeholder attribute as fallback', () => {
instance.element.removeChild(instance.element.firstChild);
expect(instance.placeholderOption).to.be.instanceOf(HTMLOptionElement);
expect(instance.placeholderOption.value).to.equal('Value 1');
});
});
@ -133,6 +148,7 @@ describe('components/wrappedSelect', () => {
selectElement.appendChild(fragment);
expect(fragment).to.be.instanceOf(DocumentFragment);
expect(instance.template.callCount).to.equal(2);
expect(selectElement.options.length).to.equal(2);
expect(selectElement.options[0].value).to.equal(options[0].value);
expect(selectElement.options[1].value).to.equal(options[1].value);
@ -142,7 +158,7 @@ describe('components/wrappedSelect', () => {
describe('appendDocFragment', () => {
it('empties contents of element', () => {
expect(instance.element.getElementsByTagName('option').length).to.equal(
4,
5,
);
instance.appendDocFragment(document.createDocumentFragment());
expect(instance.element.getElementsByTagName('option').length).to.equal(

View file

@ -22,7 +22,6 @@ export const DEFAULT_CLASSNAMES = {
openState: 'is-open',
disabledState: 'is-disabled',
highlightedState: 'is-highlighted',
hiddenState: 'is-hidden',
flippedState: 'is-flipped',
loadingState: 'is-loading',
noResults: 'has-no-results',
@ -36,7 +35,7 @@ export const DEFAULT_CONFIG = {
renderChoiceLimit: -1,
maxItemCount: -1,
addItems: true,
addItemFilterFn: null,
addItemFilter: null,
removeItems: true,
removeItemButton: false,
editItems: false,

View file

@ -35,7 +35,6 @@ describe('constants', () => {
'openState',
'disabledState',
'highlightedState',
'hiddenState',
'flippedState',
'loadingState',
'noResults',
@ -56,7 +55,7 @@ describe('constants', () => {
expect(DEFAULT_CONFIG.renderChoiceLimit).to.be.a('number');
expect(DEFAULT_CONFIG.maxItemCount).to.be.a('number');
expect(DEFAULT_CONFIG.addItems).to.be.a('boolean');
expect(DEFAULT_CONFIG.addItemFilterFn).to.equal(null);
expect(DEFAULT_CONFIG.addItemFilter).to.equal(null);
expect(DEFAULT_CONFIG.removeItems).to.be.a('boolean');
expect(DEFAULT_CONFIG.removeItemButton).to.be.a('boolean');
expect(DEFAULT_CONFIG.editItems).to.be.a('boolean');

View file

@ -0,0 +1,45 @@
window.delegateEvent = (function delegateEvent() {
let events;
let addedListenerTypes;
if (typeof events === 'undefined') {
events = new Map();
}
if (typeof addedListenerTypes === 'undefined') {
addedListenerTypes = [];
}
function _callback(event) {
const type = events.get(event.type);
if (!type) {
return;
}
type.forEach(fn => fn(event));
}
return {
add: function add(type, fn) {
// Cache list of events.
if (events.has(type)) {
events.get(type).push(fn);
} else {
events.set(type, [fn]);
}
// Setup events.
if (addedListenerTypes.indexOf(type) === -1) {
document.documentElement.addEventListener(type, _callback, true);
addedListenerTypes.push(type);
}
},
remove: function remove(type, fn) {
if (!events.get(type)) {
return;
}
events.set(type, events.get(type).filter(item => item !== fn));
if (!events.get(type).length) {
addedListenerTypes.splice(addedListenerTypes.indexOf(type), 1);
}
},
};
})();

View file

@ -1,16 +1,8 @@
export const getRandomNumber = (min, max) =>
Math.floor(Math.random() * (max - min) + min);
export const generateChars = length => {
let chars = '';
for (let i = 0; i < length; i++) {
const randomChar = getRandomNumber(0, 36);
chars += randomChar.toString(36);
}
return chars;
};
export const generateChars = length =>
Array.from({ length }, () => getRandomNumber(0, 36).toString(36)).join('');
export const generateId = (element, prefix) => {
let id =
@ -28,44 +20,46 @@ export const getType = obj => Object.prototype.toString.call(obj).slice(8, -1);
export const isType = (type, obj) =>
obj !== undefined && obj !== null && getType(obj) === type;
export const isElement = element => element instanceof Element;
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);
};
export const findAncestorByAttrName = (el, attr) => {
let target = el;
/**
* @param {HTMLElement} el
* @param {string} attr
*/
export const findAncestorByAttrName = (el, attr) => el.closest(`[${attr}]`);
while (target) {
if (target.hasAttribute(attr)) {
return target;
export const getAdjacentEl =
/**
* @param {Element} startEl
* @param {string} selector
* @param {1 | -1} direction
* @returns {Element | undefined}
*/
(startEl, selector, direction = 1) => {
if (!(startEl instanceof Element) || typeof selector !== 'string') {
return undefined;
}
target = target.parentElement;
}
const prop = `${direction > 0 ? 'next' : 'previous'}ElementSibling`;
return null;
};
let sibling = startEl[prop];
while (sibling) {
if (sibling.matches(selector)) {
return sibling;
}
sibling = sibling[prop];
}
export const getAdjacentEl = (startEl, className, direction = 1) => {
if (!startEl || !className) {
return;
}
const parent = startEl.parentNode.parentNode;
const children = Array.from(parent.querySelectorAll(className));
const startPos = children.indexOf(startEl);
const operatorDirection = direction > 0 ? 1 : -1;
return children[startPos + operatorDirection];
};
return sibling;
};
export const isScrolledIntoView = (el, parent, direction = 1) => {
if (!el) {
@ -87,7 +81,7 @@ export const isScrolledIntoView = (el, parent, direction = 1) => {
};
export const sanitise = value => {
if (!isType('String', value)) {
if (typeof value !== 'string') {
return value;
}
@ -100,6 +94,7 @@ export const sanitise = value => {
export const strToEl = (() => {
const tmpEl = document.createElement('div');
return str => {
const cleanedInput = str.trim();
tmpEl.innerHTML = cleanedInput;
@ -113,67 +108,18 @@ export const strToEl = (() => {
};
})();
/**
* Determines the width of a passed input based on its value and passes
* it to the supplied callback function.
*/
export const calcWidthOfInput = (input, callback) => {
const value = input.value || input.placeholder;
let width = input.offsetWidth;
if (value) {
const testEl = strToEl(`<span>${sanitise(value)}</span>`);
testEl.style.position = 'absolute';
testEl.style.padding = '0';
testEl.style.top = '-9999px';
testEl.style.left = '-9999px';
testEl.style.width = 'auto';
testEl.style.whiteSpace = 'pre';
if (document.body.contains(input) && window.getComputedStyle) {
const inputStyle = window.getComputedStyle(input);
if (inputStyle) {
testEl.style.fontSize = inputStyle.fontSize;
testEl.style.fontFamily = inputStyle.fontFamily;
testEl.style.fontWeight = inputStyle.fontWeight;
testEl.style.fontStyle = inputStyle.fontStyle;
testEl.style.letterSpacing = inputStyle.letterSpacing;
testEl.style.textTransform = inputStyle.textTransform;
testEl.style.padding = inputStyle.padding;
}
}
document.body.appendChild(testEl);
requestAnimationFrame(() => {
if (value && testEl.offsetWidth !== input.offsetWidth) {
width = testEl.offsetWidth + 4;
}
document.body.removeChild(testEl);
callback.call(this, `${width}px`);
export const sortByAlpha =
/**
* @param {{ label?: string, value: string }} a
* @param {{ label?: string, value: string }} b
* @returns {number}
*/
({ value, label = value }, { value: value2, label: label2 = value2 }) =>
label.localeCompare(label2, [], {
sensitivity: 'base',
ignorePunctuation: true,
numeric: true,
});
} else {
callback.call(this, `${width}px`);
}
};
export const sortByAlpha = (a, b) => {
const labelA = `${a.label || a.value}`.toLowerCase();
const labelB = `${b.label || b.value}`.toLowerCase();
if (labelA < labelB) {
return -1;
}
if (labelA > labelB) {
return 1;
}
return 0;
};
export const sortByScore = (a, b) => a.score - b.score;
@ -187,40 +133,12 @@ export const dispatchEvent = (element, type, customArgs = null) => {
return element.dispatchEvent(event);
};
export const getWindowHeight = () => {
const body = document.body;
const html = document.documentElement;
return Math.max(
body.scrollHeight,
body.offsetHeight,
html.clientHeight,
html.scrollHeight,
html.offsetHeight,
);
};
export const fetchFromObject = (object, path) => {
const index = path.indexOf('.');
if (index > -1) {
return fetchFromObject(
object[path.substring(0, index)],
path.substr(index + 1),
);
}
return object[path];
};
export const isIE11 = () =>
!!(
navigator.userAgent.match(/Trident/) &&
navigator.userAgent.match(/rv[ :]11/)
);
export const isIE11 = userAgent =>
!!(userAgent.match(/Trident/) && userAgent.match(/rv[ :]11/));
export const existsInArray = (array, value, key = 'value') =>
array.some(item => {
if (isType('String', value)) {
if (typeof value === 'string') {
return item[key] === value.trim();
}

View file

@ -6,11 +6,10 @@ import {
generateId,
getType,
isType,
isElement,
sanitise,
sortByAlpha,
sortByScore,
fetchFromObject,
isIE11,
existsInArray,
cloneObject,
dispatchEvent,
@ -97,14 +96,6 @@ describe('utils', () => {
});
});
describe('isElement', () => {
it('checks with given object is an element', () => {
const element = document.createElement('div');
expect(isElement(element)).to.equal(true);
expect(isElement({})).to.equal(false);
});
});
describe('sanitise', () => {
it('strips HTML from value', () => {
const value = '<script>somethingMalicious();</script>';
@ -198,16 +189,15 @@ describe('utils', () => {
});
});
describe('fetchFromObject', () => {
it('fetches value from object using given path', () => {
const object = {
band: {
name: 'The Strokes',
},
};
describe('isIE11', () => {
it('returns whether the given user agent string matches an IE11 user agent string', () => {
const IE11UserAgent =
'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko';
const firefoxUserAgent =
'Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0';
const output = fetchFromObject(object, 'band.name');
expect(output).to.equal(object.band.name);
expect(isIE11(IE11UserAgent)).to.equal(true);
expect(isIE11(firefoxUserAgent)).to.equal(false);
});
});

View file

@ -33,6 +33,7 @@ export default function choices(state = defaultState, action) {
return state.map(obj => {
const choice = obj;
choice.active = action.active;
return choice;
});
}
@ -45,6 +46,7 @@ export default function choices(state = defaultState, action) {
if (choice.id === parseInt(action.choiceId, 10)) {
choice.selected = true;
}
return choice;
});
}
@ -61,6 +63,7 @@ export default function choices(state = defaultState, action) {
if (choice.id === parseInt(action.choiceId, 10)) {
choice.selected = false;
}
return choice;
});
}
@ -76,8 +79,10 @@ export default function choices(state = defaultState, action) {
choice.active = action.results.some(({ item, score }) => {
if (item.id === choice.id) {
choice.score = score;
return true;
}
return false;
});
@ -89,6 +94,7 @@ export default function choices(state = defaultState, action) {
return state.map(obj => {
const choice = obj;
choice.active = action.active;
return choice;
});
}

View file

@ -23,6 +23,7 @@ export default function items(state = defaultState, action) {
return newState.map(obj => {
const item = obj;
item.highlighted = false;
return item;
});
}
@ -34,6 +35,7 @@ export default function items(state = defaultState, action) {
if (item.id === action.id) {
item.active = false;
}
return item;
});
}
@ -44,6 +46,7 @@ export default function items(state = defaultState, action) {
if (item.id === action.id) {
item.highlighted = action.highlighted;
}
return item;
});
}

View file

@ -1,5 +1,5 @@
import { createStore } from 'redux';
import rootReducer from './../reducers/index';
import rootReducer from '../reducers/index';
export default class Store {
constructor() {
@ -73,7 +73,7 @@ export default class Store {
* @return {Array} Option objects
*/
get activeChoices() {
const choices = this.choices;
const { choices } = this;
const values = choices.filter(choice => choice.active === true);
return values;
@ -118,14 +118,14 @@ export default class Store {
* @return {Array} Group objects
*/
get activeGroups() {
const groups = this.groups;
const choices = this.choices;
const { groups, choices } = this;
return groups.filter(group => {
const isActive = group.active === true && group.disabled === false;
const hasActiveOptions = choices.some(
choice => choice.active === true && choice.disabled === false,
);
return isActive && hasActiveOptions;
}, []);
}
@ -140,16 +140,16 @@ export default class Store {
/**
* Get single choice by it's ID
* @return {Object} Found choice
* @param {id} string
* @return {import('../../../types/index').Choices.Choice | false} Found choice
*/
getChoiceById(id) {
if (id) {
const choices = this.activeChoices;
const foundChoice = choices.find(
choice => choice.id === parseInt(id, 10),
);
return foundChoice;
const n = parseInt(id, 10);
return this.activeChoices.find(choice => choice.id === n);
}
return false;
}

View file

@ -1,238 +1,248 @@
import classNames from 'classnames';
import { strToEl } from './lib/utils';
/**
* Helpers to create HTML elements used by Choices
* Can be overridden by providing `callbackOnCreateTemplates` option
* @typedef {import('../../types/index').Choices.Templates} Templates
*/
export const TEMPLATES = {
export const TEMPLATES = /** @type {Templates} */ ({
containerOuter(
globalClasses,
direction,
{ containerOuter },
dir,
isSelectElement,
isSelectOneElement,
searchEnabled,
passedElementType,
) {
const tabIndex = isSelectOneElement ? 'tabindex="0"' : '';
let role = isSelectElement ? 'role="listbox"' : '';
let ariaAutoComplete = '';
const div = Object.assign(document.createElement('div'), {
className: containerOuter,
});
if (isSelectElement && searchEnabled) {
role = 'role="combobox"';
ariaAutoComplete = 'aria-autocomplete="list"';
div.dataset.type = passedElementType;
if (dir) {
div.dir = dir;
}
return strToEl(`
<div
class="${globalClasses.containerOuter}"
data-type="${passedElementType}"
${role}
${tabIndex}
${ariaAutoComplete}
aria-haspopup="true"
aria-expanded="false"
dir="${direction}"
>
</div>
`);
if (isSelectOneElement) {
div.tabIndex = 0;
}
if (isSelectElement) {
div.setAttribute('role', searchEnabled ? 'combobox' : 'listbox');
if (searchEnabled) {
div.setAttribute('aria-autocomplete', 'list');
}
}
div.setAttribute('aria-haspopup', 'true');
div.setAttribute('aria-expanded', 'false');
return div;
},
containerInner(globalClasses) {
return strToEl(`
<div class="${globalClasses.containerInner}"></div>
`);
containerInner({ containerInner }) {
return Object.assign(document.createElement('div'), {
className: containerInner,
});
},
itemList(globalClasses, isSelectOneElement) {
const localClasses = classNames(globalClasses.list, {
[globalClasses.listSingle]: isSelectOneElement,
[globalClasses.listItems]: !isSelectOneElement,
itemList({ list, listSingle, listItems }, isSelectOneElement) {
return Object.assign(document.createElement('div'), {
className: `${list} ${isSelectOneElement ? listSingle : listItems}`,
});
},
placeholder({ placeholder }, value) {
return Object.assign(document.createElement('div'), {
className: placeholder,
innerHTML: value,
});
},
item(
{ item, button, highlightedState, itemSelectable, placeholder },
{
id,
value,
label,
customProperties,
active,
disabled,
highlighted,
placeholder: isPlaceholder,
},
removeItemButton,
) {
const div = Object.assign(document.createElement('div'), {
className: item,
innerHTML: label,
});
return strToEl(`
<div class="${localClasses}"></div>
`);
},
placeholder(globalClasses, value) {
return strToEl(`
<div class="${globalClasses.placeholder}">
${value}
</div>
`);
},
item(globalClasses, data, removeItemButton) {
const ariaSelected = data.active ? 'aria-selected="true"' : '';
const ariaDisabled = data.disabled ? 'aria-disabled="true"' : '';
let localClasses = classNames(globalClasses.item, {
[globalClasses.highlightedState]: data.highlighted,
[globalClasses.itemSelectable]: !data.highlighted,
[globalClasses.placeholder]: data.placeholder,
Object.assign(div.dataset, {
item: '',
id,
value,
customProperties,
});
if (active) {
div.setAttribute('aria-selected', 'true');
}
if (disabled) {
div.setAttribute('aria-disabled', 'true');
}
if (isPlaceholder) {
div.classList.add(placeholder);
}
div.classList.add(highlighted ? highlightedState : itemSelectable);
if (removeItemButton) {
localClasses = classNames(globalClasses.item, {
[globalClasses.highlightedState]: data.highlighted,
[globalClasses.itemSelectable]: !data.disabled,
[globalClasses.placeholder]: data.placeholder,
if (disabled) {
div.classList.remove(itemSelectable);
}
div.dataset.deletable = '';
/** @todo This MUST be localizable, not hardcoded! */
const REMOVE_ITEM_TEXT = 'Remove item';
const removeButton = Object.assign(document.createElement('button'), {
type: 'button',
className: button,
innerHTML: REMOVE_ITEM_TEXT,
});
return strToEl(`
<div
class="${localClasses}"
data-item
data-id="${data.id}"
data-value="${data.value}"
data-custom-properties='${data.customProperties}'
data-deletable
${ariaSelected}
${ariaDisabled}
>
${data.label}<!--
--><button
type="button"
class="${globalClasses.button}"
data-button
aria-label="Remove item: '${data.value}'"
>
Remove item
</button>
</div>
`);
removeButton.setAttribute(
'aria-label',
`${REMOVE_ITEM_TEXT}: '${value}'`,
);
removeButton.dataset.button = '';
div.appendChild(removeButton);
}
return strToEl(`
<div
class="${localClasses}"
data-item
data-id="${data.id}"
data-value="${data.value}"
${ariaSelected}
${ariaDisabled}
>
${data.label}
</div>
`);
return div;
},
choiceList(globalClasses, isSelectOneElement) {
const ariaMultiSelectable = !isSelectOneElement
? 'aria-multiselectable="true"'
: '';
return strToEl(`
<div
class="${globalClasses.list}"
dir="ltr"
role="listbox"
${ariaMultiSelectable}
>
</div>
`);
},
choiceGroup(globalClasses, data) {
const ariaDisabled = data.disabled ? 'aria-disabled="true"' : '';
const localClasses = classNames(globalClasses.group, {
[globalClasses.itemDisabled]: data.disabled,
choiceList({ list }, isSelectOneElement) {
const div = Object.assign(document.createElement('div'), {
className: list,
});
return strToEl(`
<div
class="${localClasses}"
data-group
data-id="${data.id}"
data-value="${data.value}"
role="group"
${ariaDisabled}
>
<div class="${globalClasses.groupHeading}">${data.value}</div>
</div>
`);
if (!isSelectOneElement) {
div.setAttribute('aria-multiselectable', 'true');
}
div.setAttribute('role', 'listbox');
return div;
},
choice(globalClasses, data, itemSelectText) {
const role = data.groupId > 0 ? 'role="treeitem"' : 'role="option"';
const localClasses = classNames(
globalClasses.item,
globalClasses.itemChoice,
{
[globalClasses.itemDisabled]: data.disabled,
[globalClasses.itemSelectable]: !data.disabled,
[globalClasses.placeholder]: data.placeholder,
},
choiceGroup({ group, groupHeading, itemDisabled }, { id, value, disabled }) {
const div = Object.assign(document.createElement('div'), {
className: `${group} ${disabled ? itemDisabled : ''}`,
});
div.setAttribute('role', 'group');
Object.assign(div.dataset, {
group: '',
id,
value,
});
if (disabled) {
div.setAttribute('aria-disabled', 'true');
}
div.appendChild(
Object.assign(document.createElement('div'), {
className: groupHeading,
innerHTML: value,
}),
);
return strToEl(`
<div
class="${localClasses}"
data-select-text="${itemSelectText}"
data-choice
data-id="${data.id}"
data-value="${data.value}"
${
data.disabled
? 'data-choice-disabled aria-disabled="true"'
: 'data-choice-selectable'
}
id="${data.elementId}"
${role}
>
${data.label}
</div>
`);
return div;
},
input(globalClasses) {
const localClasses = classNames(
globalClasses.input,
globalClasses.inputCloned,
);
return strToEl(`
<input
type="text"
class="${localClasses}"
autocomplete="off"
autocapitalize="off"
spellcheck="false"
role="textbox"
aria-autocomplete="list"
>
`);
},
dropdown(globalClasses) {
const localClasses = classNames(
globalClasses.list,
globalClasses.listDropdown,
);
choice(
{ item, itemChoice, itemSelectable, itemDisabled, placeholder },
{
id,
value,
label,
groupId,
elementId,
disabled,
placeholder: isPlaceholder,
},
selectText,
) {
const div = Object.assign(document.createElement('div'), {
id: elementId,
innerHTML: label,
className: `${item} ${itemChoice} ${
disabled ? itemDisabled : itemSelectable
} ${isPlaceholder ? placeholder : ''}`,
});
return strToEl(`
<div
class="${localClasses}"
aria-expanded="false"
>
</div>
`);
},
notice(globalClasses, label, type = '') {
const localClasses = classNames(
globalClasses.item,
globalClasses.itemChoice,
{
[globalClasses.noResults]: type === 'no-results',
[globalClasses.noChoices]: type === 'no-choices',
},
);
div.setAttribute('role', groupId > 0 ? 'treeitem' : 'option');
return strToEl(`
<div class="${localClasses}">
${label}
</div>
`);
Object.assign(div.dataset, {
choice: '',
id,
value,
selectText,
});
if (disabled) {
div.dataset.choiceDisabled = '';
div.setAttribute('aria-disabled', 'true');
} else {
div.dataset.choiceSelectable = '';
}
return div;
},
option(data) {
return strToEl(`
<option value="${data.value}" ${data.active ? 'selected' : ''} ${
data.disabled ? 'disabled' : ''
} ${
data.customProperties
? `data-custom-properties=${data.customProperties}`
: ''
}>${data.label}</option>
`);
input({ input, inputCloned }, placeholderValue) {
const inp = Object.assign(document.createElement('input'), {
type: 'text',
className: `${input} ${inputCloned}`,
autocomplete: 'off',
autocapitalize: 'off',
spellcheck: false,
});
inp.setAttribute('role', 'textbox');
inp.setAttribute('aria-autocomplete', 'list');
inp.setAttribute('aria-label', placeholderValue);
return inp;
},
};
dropdown({ list, listDropdown }) {
const div = document.createElement('div');
div.classList.add(list, listDropdown);
div.setAttribute('aria-expanded', 'false');
return div;
},
notice({ item, itemChoice, noResults, noChoices }, innerHTML, type = '') {
const classes = [item, itemChoice];
if (type === 'no-choices') {
classes.push(noChoices);
} else if (type === 'no-results') {
classes.push(noResults);
}
return Object.assign(document.createElement('div'), {
innerHTML,
className: classes.join(' '),
});
},
option({ label, value, customProperties, active, disabled }) {
const opt = new Option(label, value, false, active);
if (customProperties) {
opt.dataset.customProperties = customProperties;
}
opt.disabled = disabled;
return opt;
},
});
export default TEMPLATES;

View file

@ -1,9 +1,25 @@
import { expect } from 'chai';
import templates from './templates';
import { getType, strToEl } from './lib/utils';
import { strToEl } from './lib/utils';
const stripElement = element =>
element.outerHTML.replace(/(^|>)\s+|\s+(?=<|$)/g, '$1');
/**
*
* @param {HTMLElement} element1
* @param {HTMLElement} element2
*/
function expectEqualElements(element1, element2) {
expect(element1.tagName).to.equal(element2.tagName);
expect(element1.attributes.length).to.equal(element2.attributes.length);
expect(Object.keys(element1.dataset)).to.have.members(
Object.keys(element2.dataset),
);
// compare attributes values
for (const attribute of Object.values(element1.attributes)) {
expect(element1.getAttribute(attribute)).to.equal(
element2.getAttribute(attribute),
);
}
}
describe('templates', () => {
describe('containerOuter', () => {
@ -40,11 +56,7 @@ describe('templates', () => {
searchEnabled,
passedElementType,
);
expect(getType(actualOutput)).to.equal('HTMLDivElement');
expect(stripElement(actualOutput)).to.equal(
stripElement(expectedOutput),
);
expectEqualElements(actualOutput, expectedOutput);
});
});
@ -75,10 +87,7 @@ describe('templates', () => {
passedElementType,
);
expect(getType(actualOutput)).to.equal('HTMLDivElement');
expect(stripElement(actualOutput)).to.equal(
stripElement(expectedOutput),
);
expectEqualElements(actualOutput, expectedOutput);
});
});
@ -110,10 +119,7 @@ describe('templates', () => {
passedElementType,
);
expect(getType(actualOutput)).to.equal('HTMLDivElement');
expect(stripElement(actualOutput)).to.equal(
stripElement(expectedOutput),
);
expectEqualElements(actualOutput, expectedOutput);
});
});
});
@ -144,10 +150,7 @@ describe('templates', () => {
passedElementType,
);
expect(getType(actualOutput)).to.equal('HTMLDivElement');
expect(stripElement(actualOutput)).to.equal(
stripElement(expectedOutput),
);
expectEqualElements(actualOutput, expectedOutput);
});
});
});
@ -162,8 +165,7 @@ describe('templates', () => {
);
const actualOutput = templates.containerInner(classes);
expect(getType(actualOutput)).to.equal('HTMLDivElement');
expect(stripElement(actualOutput)).to.equal(stripElement(expectedOutput));
expectEqualElements(actualOutput, expectedOutput);
});
});
@ -181,10 +183,7 @@ describe('templates', () => {
);
const actualOutput = templates.itemList(classes, true);
expect(getType(actualOutput)).to.equal('HTMLDivElement');
expect(stripElement(actualOutput)).to.equal(
stripElement(expectedOutput),
);
expectEqualElements(actualOutput, expectedOutput);
});
});
@ -195,10 +194,7 @@ describe('templates', () => {
);
const actualOutput = templates.itemList(classes, false);
expect(getType(actualOutput)).to.equal('HTMLDivElement');
expect(stripElement(actualOutput)).to.equal(
stripElement(expectedOutput),
);
expectEqualElements(actualOutput, expectedOutput);
});
});
});
@ -213,15 +209,10 @@ describe('templates', () => {
<div class="${classes.placeholder}">${value}</div>`);
const actualOutput = templates.placeholder(classes, value);
expect(getType(actualOutput)).to.equal('HTMLDivElement');
expect(stripElement(actualOutput)).to.equal(stripElement(expectedOutput));
expectEqualElements(actualOutput, expectedOutput);
});
});
// describe('item', () => {
// });
describe('choiceList', () => {
const classes = {
list: 'test',
@ -232,17 +223,13 @@ describe('templates', () => {
const expectedOutput = strToEl(`
<div
class="${classes.list}"
dir="ltr"
role="listbox"
>
</div>
`);
const actualOutput = templates.choiceList(classes, true);
expect(getType(actualOutput)).to.equal('HTMLDivElement');
expect(stripElement(actualOutput)).to.equal(
stripElement(expectedOutput),
);
expectEqualElements(actualOutput, expectedOutput);
});
});
@ -251,7 +238,6 @@ describe('templates', () => {
const expectedOutput = strToEl(`
<div
class="${classes.list}"
dir="ltr"
role="listbox"
aria-multiselectable="true"
>
@ -259,10 +245,7 @@ describe('templates', () => {
`);
const actualOutput = templates.choiceList(classes, false);
expect(getType(actualOutput)).to.equal('HTMLDivElement');
expect(stripElement(actualOutput)).to.equal(
stripElement(expectedOutput),
);
expectEqualElements(actualOutput, expectedOutput);
});
});
});
@ -299,10 +282,7 @@ describe('templates', () => {
`);
const actualOutput = templates.choiceGroup(classes, data);
expect(getType(actualOutput)).to.equal('HTMLDivElement');
expect(stripElement(actualOutput)).to.equal(
stripElement(expectedOutput),
);
expectEqualElements(actualOutput, expectedOutput);
});
});
@ -329,10 +309,7 @@ describe('templates', () => {
`);
const actualOutput = templates.choiceGroup(classes, data);
expect(getType(actualOutput)).to.equal('HTMLDivElement');
expect(stripElement(actualOutput)).to.equal(
stripElement(expectedOutput),
);
expectEqualElements(actualOutput, expectedOutput);
});
});
});
@ -365,9 +342,7 @@ describe('templates', () => {
it('returns expected html', () => {
const expectedOutput = strToEl(`
<div
class="${classes.item} ${classes.itemChoice} ${
classes.itemSelectable
}"
class="${classes.item} ${classes.itemChoice} ${classes.itemSelectable}"
data-select-text="${itemSelectText}"
data-choice
data-id="${data.id}"
@ -381,10 +356,7 @@ describe('templates', () => {
`);
const actualOutput = templates.choice(classes, data, itemSelectText);
expect(getType(actualOutput)).to.equal('HTMLDivElement');
expect(stripElement(actualOutput)).to.equal(
stripElement(expectedOutput),
);
expectEqualElements(actualOutput, expectedOutput);
});
});
@ -399,9 +371,7 @@ describe('templates', () => {
it('returns expected html', () => {
const expectedOutput = strToEl(`
<div
class="${classes.item} ${classes.itemChoice} ${
classes.itemDisabled
}"
class="${classes.item} ${classes.itemChoice} ${classes.itemDisabled}"
data-select-text="${itemSelectText}"
data-choice
data-id="${data.id}"
@ -416,10 +386,7 @@ describe('templates', () => {
`);
const actualOutput = templates.choice(classes, data, itemSelectText);
expect(getType(actualOutput)).to.equal('HTMLDivElement');
expect(stripElement(actualOutput)).to.equal(
stripElement(expectedOutput),
);
expectEqualElements(actualOutput, expectedOutput);
});
});
@ -434,9 +401,7 @@ describe('templates', () => {
it('returns expected html', () => {
const expectedOutput = strToEl(`
<div
class="${classes.item} ${classes.itemChoice} ${
classes.itemSelectable
} ${classes.placeholder}"
class="${classes.item} ${classes.itemChoice} ${classes.itemSelectable} ${classes.placeholder}"
data-select-text="${itemSelectText}"
data-choice
data-id="${data.id}"
@ -450,10 +415,7 @@ describe('templates', () => {
`);
const actualOutput = templates.choice(classes, data, itemSelectText);
expect(getType(actualOutput)).to.equal('HTMLDivElement');
expect(stripElement(actualOutput)).to.equal(
stripElement(expectedOutput),
);
expectEqualElements(actualOutput, expectedOutput);
});
});
@ -468,9 +430,7 @@ describe('templates', () => {
it('returns expected html', () => {
const expectedOutput = strToEl(`
<div
class="${classes.item} ${classes.itemChoice} ${
classes.itemSelectable
}"
class="${classes.item} ${classes.itemChoice} ${classes.itemSelectable}"
data-select-text="${itemSelectText}"
data-choice
data-id="${data.id}"
@ -484,10 +444,7 @@ describe('templates', () => {
`);
const actualOutput = templates.choice(classes, data, itemSelectText);
expect(getType(actualOutput)).to.equal('HTMLDivElement');
expect(stripElement(actualOutput)).to.equal(
stripElement(expectedOutput),
);
expectEqualElements(actualOutput, expectedOutput);
});
});
});
@ -499,49 +456,49 @@ describe('templates', () => {
};
it('returns expected html', () => {
/*
Following attributes are not supported by JSDOM, so, can't compare
autocapitalize="off"
spellcheck="false"
*/
const expectedOutput = strToEl(`
<input
type="text"
class="${classes.input} ${classes.inputCloned}"
autocomplete="off"
autocapitalize="off"
spellcheck="false"
role="textbox"
aria-autocomplete="list"
aria-label="test placeholder"
>
`);
const actualOutput = templates.input(classes);
const actualOutput = templates.input(classes, 'test placeholder');
expect(getType(actualOutput)).to.equal('HTMLInputElement');
expect(stripElement(actualOutput)).to.equal(stripElement(expectedOutput));
expectEqualElements(actualOutput, expectedOutput);
});
});
describe('dropdown', () => {
const classes = {
list: 'test 1',
listDropdown: 'test 2',
list: 'test-1',
listDropdown: 'test-2',
};
it('returns expected html', () => {
const value = 'test';
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);
expect(getType(actualOutput)).to.equal('HTMLDivElement');
expect(stripElement(actualOutput)).to.equal(stripElement(expectedOutput));
expectEqualElements(actualOutput, expectedOutput);
});
});
describe('notice', () => {
const classes = {
item: 'test 1',
itemChoice: 'test 2',
noResults: 'test 3',
noChoices: 'test 4',
item: 'test-1',
itemChoice: 'test-2',
noResults: 'test-3',
noChoices: 'test-4',
};
const label = 'test';
@ -554,42 +511,33 @@ describe('templates', () => {
`);
const actualOutput = templates.notice(classes, label);
expect(getType(actualOutput)).to.equal('HTMLDivElement');
expect(stripElement(actualOutput)).to.equal(stripElement(expectedOutput));
expectEqualElements(actualOutput, expectedOutput);
});
describe('passing a notice type', () => {
describe('no results', () => {
it('adds no results classname', () => {
const expectedOutput = strToEl(`
<div class="${classes.item} ${classes.itemChoice} ${
classes.noResults
}">
<div class="${classes.item} ${classes.itemChoice} ${classes.noResults}">
${label}
</div>
`);
const actualOutput = templates.notice(classes, label, 'no-results');
expect(stripElement(actualOutput)).to.equal(
stripElement(expectedOutput),
);
expectEqualElements(actualOutput, expectedOutput);
});
});
describe('no choices', () => {
it('adds no choices classname', () => {
const expectedOutput = strToEl(`
<div class="${classes.item} ${classes.itemChoice} ${
classes.noChoices
}">
<div class="${classes.item} ${classes.itemChoice} ${classes.noChoices}">
${label}
</div>
`);
const actualOutput = templates.notice(classes, label, 'no-choices');
expect(stripElement(actualOutput)).to.equal(
stripElement(expectedOutput),
);
expectEqualElements(actualOutput, expectedOutput);
});
});
});
@ -615,8 +563,7 @@ describe('templates', () => {
);
const actualOutput = templates.option(data);
expect(getType(actualOutput)).to.equal('HTMLOptionElement');
expect(stripElement(actualOutput)).to.equal(stripElement(expectedOutput));
expectEqualElements(actualOutput, expectedOutput);
});
describe('when selected', () => {

View file

@ -2,23 +2,23 @@
= Generic styling =
=============================================*/
$global-guttering : 24px;
$global-font-size-h1 : 32px;
$global-font-size-h2 : 24px;
$global-font-size-h3 : 20px;
$global-font-size-h4 : 18px;
$global-font-size-h5 : 16px;
$global-font-size-h6 : 14px;
$global-guttering: 24px;
$global-font-size-h1: 32px;
$global-font-size-h2: 24px;
$global-font-size-h3: 20px;
$global-font-size-h4: 18px;
$global-font-size-h5: 16px;
$global-font-size-h6: 14px;
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale
-moz-osx-font-smoothing: grayscale;
}
*,
*:before,
*:after {
box-sizing: border-box
box-sizing: border-box;
}
html,
@ -30,10 +30,10 @@ body {
}
body {
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
font-family: 'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif;
font-size: 16px;
line-height: 1.4;
color: #FFFFFF;
color: #ffffff;
background-color: #333;
overflow-x: hidden;
}
@ -52,7 +52,7 @@ p {
hr {
display: block;
margin: $global-guttering*1.25 0;
margin: $global-guttering * 1.25 0;
border: 0;
border-bottom: 1px solid #eaeaea;
height: 1px;
@ -73,7 +73,7 @@ h6 {
a,
a:visited,
a:focus {
color: #FFFFFF;
color: #ffffff;
text-decoration: none;
font-weight: 600;
}
@ -133,14 +133,14 @@ label + p {
display: block;
margin: auto;
max-width: 40em;
padding: $global-guttering*2;
padding: $global-guttering * 2;
@media (max-width: 620px) {
padding: 0;
}
}
.section {
background-color: #FFFFFF;
background-color: #ffffff;
padding: $global-guttering;
color: #333;
a,
@ -184,12 +184,8 @@ label + p {
text-align: center;
}
.is-hidden {
display: none;
}
[data-test-hook] {
margin-bottom: $global-guttering;
}
/*===== End of Section comment block ======*/
/*===== End of Section comment block ======*/

View file

@ -10,11 +10,11 @@ $choices-guttering: 24px !default;
$choices-border-radius: 2.5px !default;
$choices-border-radius-item: 20px !default;
$choices-bg-color: #f9f9f9 !default;
$choices-bg-color-disabled: #EAEAEA !default;
$choices-bg-color-dropdown: #FFFFFF !default;
$choices-bg-color-disabled: #eaeaea !default;
$choices-bg-color-dropdown: #ffffff !default;
$choices-text-color: #333333 !default;
$choices-keyline-color: #DDDDDD !default;
$choices-primary-color: #00BCD4 !default;
$choices-keyline-color: #dddddd !default;
$choices-primary-color: #00bcd4 !default;
$choices-disabled-color: #eaeaea !default;
$choices-highlight-color: $choices-primary-color !default;
$choices-button-dimension: 8px !default;
@ -43,9 +43,13 @@ $choices-icon-cross-inverse: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiI
cursor: not-allowed;
}
}
[hidden] {
display: none !important;
}
}
.#{$choices-selector}[data-type*="select-one"] {
.#{$choices-selector}[data-type*='select-one'] {
cursor: pointer;
.#{$choices-selector}__inner {
padding-bottom: 7.5px;
@ -55,7 +59,7 @@ $choices-icon-cross-inverse: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiI
width: 100%;
padding: 10px;
border-bottom: 1px solid $choices-keyline-color;
background-color: #FFFFFF;
background-color: #ffffff;
margin: 0;
}
.#{$choices-selector}__button {
@ -70,7 +74,7 @@ $choices-icon-cross-inverse: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiI
height: 20px;
width: 20px;
border-radius: 10em;
opacity: .5;
opacity: 0.5;
&:hover,
&:focus {
opacity: 1;
@ -80,7 +84,7 @@ $choices-icon-cross-inverse: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiI
}
}
&:after {
content: "";
content: '';
height: 0;
width: 0;
border-style: solid;
@ -96,7 +100,7 @@ $choices-icon-cross-inverse: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiI
border-color: transparent transparent $choices-text-color transparent;
margin-top: -7.5px;
}
&[dir="rtl"] {
&[dir='rtl'] {
&:after {
left: 11.5px;
right: auto;
@ -110,8 +114,8 @@ $choices-icon-cross-inverse: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiI
}
}
.#{$choices-selector}[data-type*="select-multiple"],
.#{$choices-selector}[data-type*="text"] {
.#{$choices-selector}[data-type*='select-multiple'],
.#{$choices-selector}[data-type*='text'] {
.#{$choices-selector}__inner {
cursor: text;
}
@ -122,13 +126,13 @@ $choices-icon-cross-inverse: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiI
margin-right: -$choices-button-offset/2;
margin-bottom: 0;
margin-left: $choices-button-offset;
padding-left: $choices-button-offset*2;
padding-left: $choices-button-offset * 2;
border-left: 1px solid darken($choices-primary-color, 10%);
background-image: $choices-icon-cross;
background-size: $choices-button-dimension;
width: $choices-button-dimension;
line-height: 1;
opacity: .75;
opacity: 0.75;
border-radius: 0;
&:hover,
&:focus {
@ -170,7 +174,7 @@ $choices-icon-cross-inverse: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiI
display: inline-block;
padding: 4px 16px 4px 4px;
width: 100%;
[dir="rtl"] & {
[dir='rtl'] & {
padding-right: 4px;
padding-left: 16px;
}
@ -192,12 +196,12 @@ $choices-icon-cross-inverse: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiI
margin-bottom: 3.75px;
background-color: $choices-primary-color;
border: 1px solid darken($choices-primary-color, 5%);
color: #FFFFFF;
color: #ffffff;
word-break: break-all;
&[data-deletable] {
padding-right: 5px;
}
[dir="rtl"] & {
[dir='rtl'] & {
margin-right: 0;
margin-left: 3.75px;
}
@ -213,7 +217,7 @@ $choices-icon-cross-inverse: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiI
}
.#{$choices-selector}__list--dropdown {
display: none;
visibility: hidden;
z-index: 1;
position: absolute;
width: 100%;
@ -225,8 +229,9 @@ $choices-icon-cross-inverse: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiI
border-bottom-right-radius: $choices-border-radius;
overflow: hidden;
word-break: break-all;
will-change: visibility;
&.is-active {
display: block;
visibility: visible;
}
.is-open & {
border-color: darken($choices-keyline-color, 15%);
@ -236,7 +241,7 @@ $choices-icon-cross-inverse: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiI
bottom: 100%;
margin-top: 0;
margin-bottom: -1px;
border-radius: .25rem .25rem 0 0;
border-radius: 0.25rem 0.25rem 0 0;
}
.#{$choices-selector}__list {
position: relative;
@ -249,7 +254,7 @@ $choices-icon-cross-inverse: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiI
position: relative;
padding: 10px;
font-size: $choices-font-size-md;
[dir="rtl"] & {
[dir='rtl'] & {
text-align: right;
}
}
@ -265,7 +270,7 @@ $choices-icon-cross-inverse: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiI
top: 50%;
transform: translateY(-50%);
}
[dir="rtl"] & {
[dir='rtl'] & {
text-align: right;
padding-left: 100px;
padding-right: 10px;
@ -276,9 +281,9 @@ $choices-icon-cross-inverse: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiI
}
}
&.is-highlighted {
background-color: mix(#000000, #FFFFFF, 5%);
background-color: mix(#000000, #ffffff, 5%);
&:after {
opacity: .5;
opacity: 0.5;
}
}
}
@ -295,7 +300,7 @@ $choices-icon-cross-inverse: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiI
.#{$choices-selector}__item--disabled {
cursor: not-allowed;
user-select: none;
opacity: .5;
opacity: 0.5;
}
.#{$choices-selector}__heading {
@ -333,20 +338,14 @@ $choices-icon-cross-inverse: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiI
&:focus {
outline: 0;
}
[dir="rtl"] & {
[dir='rtl'] & {
padding-right: 2px;
padding-left: 0;
}
}
.#{$choices-selector}__placeholder {
opacity: .5;
opacity: 0.5;
}
.#{$choices-selector}__input.is-hidden,
.#{$choices-selector}[data-type*="select-one"] .#{$choices-selector}__input.is-hidden,
.#{$choices-selector}[data-type*="select-multiple"] .#{$choices-selector}__input.is-hidden {
display: none;
}
/*===== End of Choices ======*/
/*===== End of Choices ======*/

520
types/index.d.ts vendored
View file

@ -1,19 +1,37 @@
// Type definitions for Choices.js 3.0.2
// Type definitions for Choices.js 7.1.x
// Project: https://github.com/jshjohnson/Choices
// Definitions by: Arthur vasconcelos <https://github.com/arthurvasconcelos>, Josh Johnson <https://github.com/jshjohnson>, Zack Schuster <https://github.com/zackschuster>
// Definitions by:
// Arthur vasconcelos <https://github.com/arthurvasconcelos>,
// Josh Johnson <https://github.com/jshjohnson>,
// Zack Schuster <https://github.com/zackschuster>
// Konstantin Vyatkin <https://github.com/tinovyatkin>
// Definitions: https://github.com/jshjohnson/Choices
// TypeScript Version: 2.6.2
import { FuseOptions } from 'fuse.js';
// Choices Namespace
declare namespace Choices {
namespace Types {
type renderSelected = 'auto' | 'always';
type dropdownPosition = 'auto' | 'top';
type strToEl = (str: string) => HTMLElement | HTMLInputElement | HTMLOptionElement;
type strToEl = (
str: string,
) => HTMLElement | HTMLInputElement | HTMLOptionElement;
type stringFunction = () => string;
type noticeStringFunction = (value: string) => string;
type noticeLimitFunction = (maxItemCount: number) => string;
type callbackOnCreateTemplates = (template: strToEl) => Choices.Templates;
type filterFunction = (value: string) => boolean;
}
interface Choice {
customProperties?: Record<string, any>;
disabled?: boolean;
elementId?: string;
groupId?: string;
id?: string;
keyCode?: number;
label: string;
placeholder?: boolean;
selected?: boolean;
value: string;
}
/**
@ -27,16 +45,13 @@ declare namespace Choices {
*
* Arguments: id, value, label, groupValue, keyCode
*/
"addItem": CustomEvent;
/**
* A filter that will need to pass for a user to successfully add an item.
*
* **Input types affected:** text
*
* @default null
*/
addItemFilterFn?: () => any;
addItem: CustomEvent<{
id: string;
value: string;
label: string;
groupValue: string;
keyCode: string;
}>;
/**
* Triggered each time an item is removed (programmatically or by the user).
@ -45,7 +60,12 @@ declare namespace Choices {
*
* Arguments: id, value, label, groupValue
*/
"removeItem": CustomEvent;
removeItem: CustomEvent<{
id: string;
value: string;
label: string;
groupValue: string;
}>;
/**
* Triggered each time an item is highlighted.
@ -54,7 +74,12 @@ declare namespace Choices {
*
* Arguments: id, value, label, groupValue
*/
"highlightItem": CustomEvent;
highlightItem: CustomEvent<{
id: string;
value: string;
label: string;
groupValue: string;
}>;
/**
* Triggered each time an item is unhighlighted.
@ -63,16 +88,21 @@ declare namespace Choices {
*
* Arguments: id, value, label, groupValue
*/
"unhighlightItem": CustomEvent;
unhighlightItem: CustomEvent<{
id: string;
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: value, keyCode
* Arguments: choice: Choice
*/
"choice": CustomEvent;
choice: CustomEvent<{ choice: Choices.Choice }>;
/**
* Triggered each time an item is added/removed **by a user**.
@ -81,7 +111,7 @@ declare namespace Choices {
*
* Arguments: value
*/
"change": CustomEvent;
change: CustomEvent<{ value: string }>;
/**
* Triggered when a user types into an input to search choices.
@ -90,7 +120,7 @@ declare namespace Choices {
*
* Arguments: value, resultCount
*/
"search": CustomEvent;
search: CustomEvent<{ value: string; resultCount: number }>;
/**
* Triggered when the dropdown is shown.
@ -99,7 +129,7 @@ declare namespace Choices {
*
* Arguments: -
*/
"showDropdown": CustomEvent;
showDropdown: CustomEvent<undefined>;
/**
* Triggered when the dropdown is hidden.
@ -108,84 +138,156 @@ declare namespace Choices {
*
* Arguments: -
*/
"hideDropdown": CustomEvent;
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: Choices.passedElement }>;
}
interface Group {
active?: boolean;
disabled?: boolean;
id?: string;
value: any;
}
interface Item extends Choice {
choiceId?: string;
keyCode?: number;
}
interface Templates {
containerOuter?: (direction: string) => HTMLElement;
containerInner?: () => HTMLElement;
itemList?: () => HTMLElement;
placeholder?: (value: string) => HTMLElement;
item?: (data: any) => HTMLElement;
choiceList?: () => HTMLElement;
choiceGroup?: (data: any) => HTMLElement;
choice?: (data: any) => HTMLElement;
input?: () => HTMLInputElement;
dropdown?: () => HTMLElement;
notice?: (label: string) => HTMLElement;
option?: (data: any) => HTMLOptionElement;
containerOuter: (
this: Choices,
classNames: ClassNames,
direction: HTMLElement['dir'],
isSelectElement: boolean,
isSelectOneElement: boolean,
searchEnabled: boolean,
passedElementType: passedElement['type'],
) => HTMLElement;
containerInner: (this: Choices, classNames: ClassNames) => HTMLElement;
itemList: (
this: Choices,
classNames: ClassNames,
isSelectOneElement: boolean,
) => HTMLElement;
placeholder: (
this: Choices,
classNames: ClassNames,
value: string,
) => HTMLElement;
item: (
this: Choices,
classNames: ClassNames,
data: Choice,
removeItemButton: boolean,
) => HTMLElement;
choiceList: (
this: Choices,
classNames: ClassNames,
isSelectOneElement: boolean,
) => HTMLElement;
choiceGroup: (
this: Choices,
classNames: ClassNames,
data: Choice,
) => HTMLElement;
choice: (
this: Choices,
classNames: ClassNames,
data: Choice,
selectText: string,
) => HTMLElement;
input: (
this: Choices,
classNames: ClassNames,
placeholderValue: string,
) => HTMLInputElement;
dropdown: (this: Choices, classNames: ClassNames) => HTMLElement;
notice: (
this: Choices,
classNames: ClassNames,
label: string,
type: '' | 'no-results' | 'no-choices',
) => HTMLElement;
option: (data: Choice) => HTMLOptionElement;
}
/** Classes added to HTML generated by Choices. By default classnames follow the BEM notation. */
interface ClassNames {
/** @default 'choices' */
containerOuter?: string;
containerOuter: string;
/** @default 'choices__inner' */
containerInner?: string;
containerInner: string;
/** @default 'choices__input' */
input?: string;
input: string;
/** @default 'choices__input--cloned' */
inputCloned?: string;
inputCloned: string;
/** @default 'choices__list' */
list?: string;
list: string;
/** @default 'choices__list--multiple' */
listItems?: string;
listItems: string;
/** @default 'choices__list--single' */
listSingle?: string;
listSingle: string;
/** @default 'choices__list--dropdown' */
listDropdown?: string;
listDropdown: string;
/** @default 'choices__item' */
item?: string;
item: string;
/** @default 'choices__item--selectable' */
itemSelectable?: string;
itemSelectable: string;
/** @default 'choices__item--disabled' */
itemDisabled?: string;
itemDisabled: string;
/** @default 'choices__item--choice' */
itemChoice?: string;
itemChoice: string;
/** @default 'choices__placeholder' */
placeholder?: string;
placeholder: string;
/** @default 'choices__group' */
group?: string;
group: string;
/** @default 'choices__heading' */
groupHeading?: string;
groupHeading: string;
/** @default 'choices__button' */
button?: string;
button: string;
/** @default 'is-active' */
activeState?: string;
activeState: string;
/** @default 'is-focused' */
focusState?: string;
focusState: string;
/** @default 'is-open' */
openState?: string;
openState: string;
/** @default 'is-disabled' */
disabledState?: string;
disabledState: string;
/** @default 'is-highlighted' */
highlightedState?: string;
/** @default 'is-hidden' */
hiddenState?: string;
highlightedState: string;
/** @default 'is-flipped' */
flippedState?: string;
flippedState: string;
/** @default 'is-loading' */
loadingState?: string;
loadingState: string;
/** @default 'has-no-results' */
noResults?: string;
noResults: string;
/** @default 'has-no-choices' */
noChoices?: string;
noChoices: string;
}
interface passedElement {
classNames: Choices.ClassNames,
element: HTMLElement,
isDisabled: boolean,
classNames: Choices.ClassNames;
element: (HTMLInputElement | HTMLSelectElement) & {
// Extends HTMLElement addEventListener with Choices events
addEventListener<K extends keyof Choices.EventMap>(
type: K,
listener: (
this: HTMLInputElement | HTMLSelectElement,
ev: Choices.EventMap[K],
) => void,
options?: boolean | AddEventListenerOptions,
): void;
};
type: 'text' | 'select-one' | 'select-multiple';
isDisabled: boolean;
parentInstance: Choices;
}
@ -194,7 +296,7 @@ declare namespace Choices {
*
* **Terminology**
*
* - **Choice:** A choice is a value a user can select. A choice would be equivelant to the `<option></option>` element within a select input.
* - **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">`
*/
@ -206,7 +308,7 @@ declare namespace Choices {
*
* @default false
*/
silent?: boolean;
silent: boolean;
/**
* Add pre-selected items (see terminology) to text input.
@ -237,7 +339,7 @@ declare namespace Choices {
*
* @default []
*/
items?: any[];
items: string[] | Choice[];
/**
* Add choices (see terminology) to select input.
@ -266,7 +368,7 @@ declare namespace Choices {
*
* @default []
*/
choices?: any[];
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.
@ -275,7 +377,7 @@ declare namespace Choices {
*
* @default -1
*/
renderChoiceLimit?: number;
renderChoiceLimit: number;
/**
* The amount of items a user can input/select `("-1" indicates no limit)`.
@ -284,7 +386,7 @@ declare namespace Choices {
*
* @default -1
*/
maxItemCount?: number;
maxItemCount: number;
/**
* Whether a user can add items.
@ -293,7 +395,28 @@ declare namespace Choices {
*
* @default true
*/
addItems?: boolean;
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 | Choices.Types.filterFunction;
/**
* 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 | Choices.Types.noticeStringFunction;
/**
* Whether a user can remove items.
@ -302,7 +425,7 @@ declare namespace Choices {
*
* @default true
*/
removeItems?: boolean;
removeItems: boolean;
/**
* Whether each item should have a remove button.
@ -311,7 +434,7 @@ declare namespace Choices {
*
* @default false
*/
removeItemButton?: boolean;
removeItemButton: boolean;
/**
* Whether a user can edit items. An item's value can be edited by pressing the backspace.
@ -320,7 +443,7 @@ declare namespace Choices {
*
* @default false
*/
editItems?: boolean;
editItems: boolean;
/**
* Whether each inputted/chosen item should be unique.
@ -329,16 +452,16 @@ declare namespace Choices {
*
* @default true
*/
duplicateItemsAllowed?: boolean;
duplicateItemsAllowed: boolean;
/**
* What divides each value. The default delimiter seperates each value with a comma: `"Value 1, Value 2, Value 3"`.
* 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;
delimiter: string;
/**
* Whether a user can paste into the input.
@ -347,7 +470,7 @@ declare namespace Choices {
*
* @default true
*/
paste?: boolean;
paste: boolean;
/**
* Whether a search area should be shown.
@ -358,7 +481,7 @@ declare namespace Choices {
*
* @default true
*/
searchEnabled?: boolean;
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.
@ -367,7 +490,7 @@ declare namespace Choices {
*
* @default true
*/
searchChoices?: boolean;
searchChoices: boolean;
/**
* The minimum length a search value should be before choices are searched.
@ -376,7 +499,7 @@ declare namespace Choices {
*
* @default 1
*/
searchFloor?: number;
searchFloor: number;
/**
* The maximum amount of search results to show.
@ -385,7 +508,7 @@ declare namespace Choices {
*
* @default 4
*/
searchResultLimit?: number;
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']`.
@ -394,7 +517,7 @@ declare namespace Choices {
*
* @default ['label', 'value']
*/
searchFields?: string[];
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.
@ -403,7 +526,7 @@ declare namespace Choices {
*
* @default 'auto'
*/
position?: Choices.Types.dropdownPosition;
position: 'auto' | 'top';
/**
* Whether the scroll position should reset after adding an item.
@ -412,7 +535,7 @@ declare namespace Choices {
*
* @default true
*/
resetScrollPosition?: boolean;
resetScrollPosition: boolean;
/**
* Whether choices and groups should be sorted. If false, choices/groups will appear in the order they were given.
@ -421,7 +544,7 @@ declare namespace Choices {
*
* @default true
*/
shouldSort?: boolean;
shouldSort: boolean;
/**
* Whether items should be sorted. If false, items will appear in the order they were selected.
@ -430,7 +553,7 @@ declare namespace Choices {
*
* @default false
*/
shouldSortItems?: boolean;
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.
@ -449,7 +572,7 @@ declare namespace Choices {
*
* @default sortByAlpha
*/
sortFilter?: (current: any, next: any) => number;
sortFilter: (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.
@ -468,7 +591,7 @@ declare namespace Choices {
*
* @default true
*/
placeholder?: boolean;
placeholder: boolean;
/**
* The value of the inputs placeholder.
@ -477,7 +600,7 @@ declare namespace Choices {
*
* @default null
*/
placeholderValue?: string;
placeholderValue: string;
/**
* The value of the search inputs placeholder.
@ -486,7 +609,7 @@ declare namespace Choices {
*
* @default null
*/
searchPlaceholderValue?: string;
searchPlaceholderValue: string;
/**
* Prepend a value to each item added/selected.
@ -495,7 +618,7 @@ declare namespace Choices {
*
* @default null
*/
prependValue?: string;
prependValue: string;
/**
* Append a value to each item added/selected.
@ -504,7 +627,7 @@ declare namespace Choices {
*
* @default null
*/
appendValue?: string;
appendValue: string;
/**
* 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`.
@ -513,7 +636,7 @@ declare namespace Choices {
*
* @default 'auto';
*/
renderSelectedChoices?: Choices.Types.renderSelected;
renderSelectedChoices: 'auto' | 'always';
/**
* The text that is shown whilst choices are being populated via AJAX.
@ -522,7 +645,7 @@ declare namespace Choices {
*
* @default 'Loading...'
*/
loadingText?: string;
loadingText: string;
/**
* The text that is shown when a user's search has returned no results. Optionally pass a function returning a string.
@ -531,7 +654,7 @@ declare namespace Choices {
*
* @default 'No results found'
*/
noResultsText?: string | Choices.Types.stringFunction;
noResultsText: string | Choices.Types.stringFunction;
/**
* The text that is shown when a user has selected all possible choices. Optionally pass a function returning a string.
@ -540,7 +663,7 @@ declare namespace Choices {
*
* @default 'No choices to choose from'
*/
noChoicesText?: string | Choices.Types.stringFunction;
noChoicesText: string | Choices.Types.stringFunction;
/**
* The text that is shown when a user hovers over a selectable choice.
@ -549,19 +672,7 @@ declare namespace Choices {
*
* @default 'Press to select'
*/
itemSelectText?: string;
/**
* 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 | Choices.Types.noticeStringFunction;
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.
@ -573,32 +684,26 @@ declare namespace Choices {
* (maxItemCount) => `Only ${maxItemCount} values can be added.`;
* ```
*/
maxItemText?: string | Choices.Types.noticeLimitFunction;
maxItemText: string | Choices.Types.noticeLimitFunction;
/**
* If no duplicates are allowed, and the value already exists in the array.
*
* @default 'Only unique values can be added.'
*/
uniqueItemText?: string | Choices.Types.noticeStringFunction;
uniqueItemText: string | Choices.Types.noticeStringFunction;
/**
* Classes added to HTML generated by Choices. By default classnames follow the BEM notation.
*
* **Input types affected:** text, select-one, select-multiple
*/
classNames?: Choices.ClassNames;
classNames: Partial<Choices.ClassNames>;
/**
* Choices uses the great Fuse library for searching. You can find more options here: https://github.com/krisk/Fuse#options
*/
fuseOptions?: {
[index: string]: any;
/**
* @default 'score'
*/
include?: string;
};
fuseOptions: FuseOptions<Choice>;
/**
* Function to run once Choices initialises.
@ -609,7 +714,7 @@ declare namespace Choices {
*
* @default null
*/
callbackOnInit?: () => any;
callbackOnInit: (this: Choices) => void;
/**
* 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].
@ -645,52 +750,32 @@ declare namespace Choices {
*
* @default null
*/
callbackOnCreateTemplates?: Choices.Types.callbackOnCreateTemplates;
callbackOnCreateTemplates: (
template: Choices.Types.strToEl,
) => Partial<Choices.Templates>;
}
}
// Overload HTMLElement addEventListener with Choices events
interface HTMLElement {
addEventListener<K extends keyof Choices.EventMap>(type: K, listener: (this: HTMLElement, ev: Choices.EventMap[K]) => any, useCapture?: boolean): void;
}
// Exporting default class
export default class Choices {
idNames: any;
config: Choices.Options;
static readonly defaults: {
readonly options: Partial<Choices.Options>;
readonly templates: Choices.Templates;
};
readonly config: Choices.Options;
// State Tracking
store: any;
initialised: boolean;
currentState: any;
prevState: any;
currentValue: string;
// Element
passedElement: Choices.passedElement;
readonly passedElement: Choices.passedElement;
// Checks
isTextElement: boolean;
isSelectOneElement: boolean;
isSelectMultipleElement: boolean;
isSelectElement: boolean;
isValidElementType: boolean;
isIe11: boolean;
isScrollingOnIe: boolean;
highlightPosition: number;
canSearch: boolean;
placeholder: boolean;
presetChoices: any[];
presetItems: any[];
readonly baseId: string;
wasTap: boolean;
constructor(element: string | HTMLElement | HTMLCollectionOf<Element> | NodeList, userConfig?: Choices.Options);
new(element?: string | HTMLElement | HTMLCollectionOf<Element> | NodeList, userConfig?: Choices.Options): this;
constructor(
selectorOrElement: string | HTMLInputElement | HTMLSelectElement,
userConfig?: Partial<Choices.Options>,
);
/**
* Creates a new instance of Choices, adds event listeners, creates templates and renders a Choices element to the DOM.
@ -749,7 +834,6 @@ export default class Choices {
*/
removeHighlightedItems(runEvent?: boolean): this;
/**
* Show option list dropdown (only affects select inputs).
*
@ -778,16 +862,47 @@ export default class Choices {
*/
getValue(valueOnly?: boolean): string | string[];
/** Direct populate choices
*
* @param {string[] | Choices.Item[]} items
*/
setValue(items: string[] | Choices.Item[]): this;
/**
* Set choices of select input via an array of objects, a value name and a label name.
* Set value of input based on existing Choice. `value` can be either a single string or an array of strings
*
* **Input types affected:** select-one, select-multiple
*
* @example
* ```
* const example = new Choices(element, {
* choices: [
* {value: 'One', label: 'Label One'},
* {value: 'Two', label: 'Label Two', disabled: true},
* {value: 'Three', label: 'Label Three'},
* ],
* });
*
* example.setChoiceByValue('Two'); // Choice with value of 'Two' has now been selected.
* ```
*/
setChoiceByValue(value: string | string[]): this;
/**
* Set choices of select input via an array of objects (or function that returns array of object or promise of it),
* a value field name and a label field name.
* This behaves the same as passing items via the choices option but can be called after initialising Choices.
* This can also be used to add groups of choices (see example 2); Optionally pass a true `replaceChoices` value to remove any existing choices.
* Optionally pass a `customProperties` object to add additional data to your choices (useful when searching/filtering etc).
*
* **Input types affected:** select-one, select-multiple
*
* @example Example 1:
* ```
* @param {string} [value = 'value'] - name of `value` field
* @param {string} [label = 'label'] - name of 'label' field
* @param {boolean} [replaceChoices = false] - whether to replace of add choices
*
* @example
* ```js
* const example = new Choices(element);
*
* example.setChoices([
@ -797,8 +912,22 @@ export default class Choices {
* ], 'value', 'label', false);
* ```
*
* @example Example 2:
* @example
* ```js
* const example = new Choices(element);
*
* example.setChoices(async () => {
* try {
* const items = await fetch('/items');
* return items.json()
* } catch(err) {
* console.error(err)
* }
* });
* ```
*
* @example
* ```js
* const example = new Choices(element);
*
* example.setChoices([{
@ -826,30 +955,21 @@ export default class Choices {
* }], 'value', 'label', false);
* ```
*/
setValue(args: string[]): this;
setChoices<
T extends object[] | ((instance: Choices) => object[] | Promise<object[]>)
>(
choices: T,
value?: string,
label?: string,
replaceChoices?: boolean,
): T extends object[] ? this : Promise<this>;
/**
* Set value of input based on existing Choice. `value` can be either a single string or an array of strings
* Clear all choices from select.
*
* **Input types affected:** select-one, select-multiple
*
* @example
* ```
* const example = new Choices(element, {
* choices: [
* {value: 'One', label: 'Label One'},
* {value: 'Two', label: 'Label Two', disabled: true},
* {value: 'Three', label: 'Label Three'},
* ],
* });
*
* example.setChoiceByValue('Two'); // Choice with value of 'Two' has now been selected.
* ```
*/
setChoiceByValue(value: string | string[]): this;
/** Direct populate choices */
setChoices(choices: any[], value: string, label: string, replaceChoices?: boolean): this;
clearChoices(): this;
/**
* Removes all items, choices and groups. Use with caution.
@ -878,40 +998,4 @@ export default class Choices {
* **Input types affected:** text, select-one, select-multiple
*/
disable(): this;
/**
* Populate choices/groups via a callback.
*
* **Input types affected:** select-one, select-multiple
*
* @example
* ```
* var example = new Choices(element);
*
* example.ajax(function(callback) {
* fetch(url)
* .then(function(response) {
* response.json().then(function(data) {
* callback(data, 'value', 'label');
* });
* })
* .catch(function(error) {
* console.log(error);
* });
* });
* ```
*/
ajax(fn: (values: any) => any): this;
/** Render group choices into a DOM fragment and append to choice list */
private createGroupsFragment(groups: any[], choices: any[], fragment: DocumentFragment): DocumentFragment;
/** Render choices into a DOM fragment and append to choice list */
private createChoicesFragment(choices: any[], fragment: DocumentFragment, withinGroup?: boolean): DocumentFragment;
/** Render items into a DOM fragment and append to items list */
private _createItemsFragment(items: any[], fragment?: DocumentFragment): void;
/** Render DOM with values */
private render(): void;
}

40
webpack.config.base.js Normal file
View file

@ -0,0 +1,40 @@
const path = require('path');
const include = path.resolve(__dirname, './src/scripts');
const exclude = /node_modules/;
/**
* @type {import('webpack').Configuration}
*/
module.exports = {
entry: ['./src/scripts/choices'],
output: {
library: 'Choices',
libraryTarget: 'window',
libraryExport: 'default',
globalObject: 'window',
},
module: {
rules: [
{
enforce: 'pre',
loader: 'eslint-loader',
test: /\.js?$/,
include,
exclude,
options: {
quiet: true,
},
},
{
loader: 'babel-loader',
test: /\.js?$/,
include,
exclude,
options: {
babelrc: true,
},
},
],
},
};

View file

@ -1,42 +1,24 @@
const path = require('path');
const { HotModuleReplacementPlugin } = require('webpack');
const deepMerge = require('deepmerge');
const baseConfig = require('./webpack.config.base');
module.exports = {
mode: 'development',
entry: [
'webpack/hot/dev-server',
'webpack-hot-middleware/client',
'./src/scripts/choices',
],
output: {
path: path.resolve('public'),
filename: 'choices.min.js',
publicPath: 'http://localhost:3001/assets/scripts/',
library: 'Choices',
libraryTarget: 'umd',
module.exports = deepMerge(
baseConfig,
/** @type {import('webpack').Configuration} */ ({
mode: 'development',
output: {
path: path.resolve(__dirname, './public'),
filename: 'choices.min.js',
publicPath: 'http://localhost:3001/assets/scripts/',
},
devtool: 'source-map',
entry: ['webpack/hot/dev-server', 'webpack-hot-middleware/client'],
plugins: [new HotModuleReplacementPlugin()],
}),
{
arrayMerge(target, source) {
return [...source, ...target];
},
},
plugins: [new HotModuleReplacementPlugin()],
module: {
rules: [
{
enforce: 'pre',
test: /\.js?$/,
include: path.join(__dirname, 'src/scripts'),
exclude: /node_modules/,
loader: 'eslint-loader',
query: {
configFile: '.eslintrc',
},
},
{
test: /\.js?$/,
include: path.join(__dirname, 'src/scripts'),
exclude: /node_modules/,
loader: 'babel-loader',
options: {
babelrc: true,
},
},
],
},
};
);

View file

@ -1,56 +1,47 @@
const path = require('path');
const WrapperPlugin = require('wrapper-webpack-plugin');
const UnminifiedWebpackPlugin = require('unminified-webpack-plugin');
const deepMerge = require('deepmerge');
const { BannerPlugin } = require('webpack');
const pkg = require('./package.json');
const baseConfig = require('./webpack.config.base');
const { name, version, author, homepage } = require('./package.json');
const banner = `/*! ${pkg.name} v${
pkg.version
} | (c) ${new Date().getFullYear()} ${pkg.author} | ${pkg.homepage} */ \n`;
const arrayMerge = (target, source) => [...source, ...target];
module.exports = {
mode: 'production',
entry: ['./src/scripts/choices'],
output: {
path: path.join(__dirname, '/public/assets/scripts'),
filename: 'choices.min.js',
publicPath: '/public/assets/scripts/',
library: 'Choices',
libraryTarget: 'umd',
auxiliaryComment: {
root: 'Window',
commonjs: 'CommonJS',
commonjs2: 'CommonJS2',
amd: 'AMD',
const prodConfig = deepMerge(
baseConfig,
{
mode: 'production',
output: {
path: path.join(__dirname, '/public/assets/scripts'),
publicPath: '/public/assets/scripts/',
},
},
plugins: [
new WrapperPlugin({
header: banner,
}),
new UnminifiedWebpackPlugin(),
],
module: {
rules: [
{
enforce: 'pre',
test: /\.js?$/,
include: path.join(__dirname, 'src/scripts'),
exclude: /(node_modules|bower_components)/,
loader: 'eslint-loader',
query: {
configFile: '.eslintrc',
},
},
{
test: /\.js?$/,
include: path.join(__dirname, 'src/scripts'),
exclude: /(node_modules|bower_components)/,
loader: 'babel-loader',
options: {
babelrc: true,
},
},
plugins: [
new BannerPlugin(
`${name} v${version} | © ${new Date().getFullYear()} ${author} | ${homepage}`,
),
],
},
};
{
arrayMerge,
},
);
module.exports = [
deepMerge(
prodConfig,
{
output: { filename: 'choices.js', libraryTarget: 'umd' },
optimization: { minimize: false },
},
{
arrayMerge,
},
),
deepMerge(
prodConfig,
{ output: { filename: 'choices.min.js' } },
{
arrayMerge,
},
),
];