Update dependencies, fix linting issues, split interfaces and default objects to resolve dependency cycles

This commit is contained in:
Matt Triff 2021-12-17 16:26:52 -05:00
parent 46deb9abe5
commit 3d921621b7
55 changed files with 22227 additions and 10336 deletions

View File

@ -3,6 +3,7 @@
"plugins": ["@typescript-eslint", "prettier", "sort-class-members"],
"extends": [
"airbnb-base",
"airbnb-typescript",
"plugin:prettier/recommended",
"plugin:compat/recommended",
"plugin:@typescript-eslint/recommended"
@ -61,7 +62,8 @@
}
],
"lines-between-class-members": "off",
"@typescript-eslint/no-namespace": "off"
"@typescript-eslint/no-namespace": "off",
"react/jsx-filename-extension": [0]
},
"overrides": [
{
@ -75,7 +77,16 @@
"no-new": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off"
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unused-expressions": "off",
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "default",
"format": ["camelCase", "PascalCase", "UPPER_CASE"],
"leadingUnderscore": "allow"
}
]
}
},
{

View File

@ -62,7 +62,7 @@ describe('Choices - select multiple', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.then($choice => {
.then(($choice) => {
selectedChoiceText = $choice.text().trim();
})
.click();
@ -72,7 +72,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=basic]')
.find('.choices__list--multiple .choices__item')
.last()
.should($item => {
.should(($item) => {
expect($item).to.contain(selectedChoiceText);
});
});
@ -80,7 +80,7 @@ describe('Choices - select multiple', () => {
it('updates the value of the original input', () => {
cy.get('[data-test-hook=basic]')
.find('.choices__input[hidden]')
.should($select => {
.should(($select) => {
expect($select.val()).to.contain(selectedChoiceText);
});
});
@ -89,7 +89,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=basic]')
.find('.choices__list--dropdown .choices__list')
.children()
.each($choice => {
.each(($choice) => {
expect($choice.text().trim()).to.not.equal(selectedChoiceText);
});
});
@ -114,7 +114,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=basic]')
.find('.choices__list--dropdown')
.should('be.visible')
.should($dropdown => {
.should(($dropdown) => {
const dropdownText = $dropdown.text().trim();
expect(dropdownText).to.equal('No choices to choose from');
});
@ -130,7 +130,7 @@ describe('Choices - select multiple', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.last()
.then($choice => {
.then(($choice) => {
removedChoiceText = $choice.text().trim();
})
.click();
@ -151,7 +151,7 @@ describe('Choices - select multiple', () => {
it('updates the value of the original input', () => {
cy.get('[data-test-hook=basic]')
.find('.choices__input[hidden]')
.should($select => {
.should(($select) => {
const val = $select.val() || [];
expect(val).to.not.contain(removedChoiceText);
});
@ -171,7 +171,7 @@ describe('Choices - select multiple', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal('Choice 2');
});
});
@ -187,7 +187,7 @@ describe('Choices - select multiple', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal('Choice 3');
});
});
@ -202,7 +202,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=basic]')
.find('.choices__list--dropdown')
.should('be.visible')
.should($dropdown => {
.should(($dropdown) => {
const dropdownText = $dropdown.text().trim();
expect(dropdownText).to.equal('No results found');
});
@ -370,7 +370,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=selection-limit]')
.find('.choices__list--dropdown')
.should('be.visible')
.should($dropdown => {
.should(($dropdown) => {
const dropdownText = $dropdown.text().trim();
expect(dropdownText).to.equal(
`Only ${selectionLimit} values can be added`,
@ -397,7 +397,7 @@ describe('Choices - select multiple', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.last()
.then($choice => {
.then(($choice) => {
selectedChoiceText = $choice.text().trim();
})
.click();
@ -407,7 +407,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=prepend-append]')
.find('.choices__list--multiple .choices__item')
.last()
.should($choice => {
.should(($choice) => {
expect($choice.data('value')).to.equal(
`before-${selectedChoiceText}-after`,
);
@ -418,7 +418,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=prepend-append]')
.find('.choices__list--multiple .choices__item')
.last()
.should($choice => {
.should(($choice) => {
expect($choice.text()).to.not.contain(
`before-${selectedChoiceText}-after`,
);
@ -460,7 +460,7 @@ describe('Choices - select multiple', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.not.contain(searchTerm);
});
});
@ -478,7 +478,7 @@ describe('Choices - select multiple', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.contain(searchTerm);
});
});
@ -565,13 +565,15 @@ describe('Choices - select multiple', () => {
});
describe('dropdown scrolling', () => {
let choicesCount;
let choicesCount: number;
// let choicesItems: number[];
beforeEach(() => {
cy.get('[data-test-hook=scrolling-dropdown]')
.find('.choices__list--dropdown .choices__list .choices__item')
.then($choices => {
.then(($choices) => {
choicesCount = $choices.length;
// choicesItems = Array.from({ length: 10 }, (_, i) => i + 1);
});
cy.get('[data-test-hook=scrolling-dropdown]')
@ -582,7 +584,7 @@ describe('Choices - select multiple', () => {
it('highlights first choice on dropdown open', () => {
cy.get('[data-test-hook=scrolling-dropdown]')
.find('.choices__list--dropdown .choices__list .is-highlighted')
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal('Choice 1');
});
});
@ -593,7 +595,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=scrolling-dropdown]')
.find('.choices__list--dropdown .choices__list .is-highlighted')
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal(`Choice ${index + 1}`);
});
@ -617,7 +619,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=scrolling-dropdown]')
.find('.choices__list--dropdown .choices__list .is-highlighted')
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal(`Choice ${index}`);
});
@ -636,7 +638,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=groups]')
.find('.choices__list--dropdown .choices__list .choices__group')
.first()
.then($group => {
.then(($group) => {
groupValue = $group.text().trim();
});
});
@ -657,7 +659,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=groups]')
.find('.choices__list--dropdown .choices__list .choices__group')
.first()
.should($group => {
.should(($group) => {
expect($group.text().trim()).to.not.equal(groupValue);
});
});
@ -688,7 +690,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=groups]')
.find('.choices__list--dropdown .choices__list .choices__group')
.first()
.should($group => {
.should(($group) => {
expect($group.text().trim()).to.equal(groupValue);
});
});
@ -728,7 +730,7 @@ describe('Choices - select multiple', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal(city);
});
@ -742,9 +744,7 @@ describe('Choices - select multiple', () => {
describe('non-string values', () => {
beforeEach(() => {
cy.get('[data-test-hook=non-string-values]')
.find('.choices')
.click();
cy.get('[data-test-hook=non-string-values]').find('.choices').click();
});
it('displays expected amount of choices in dropdown', () => {
@ -760,7 +760,7 @@ describe('Choices - select multiple', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.then($choice => {
.then(($choice) => {
$selectedChoice = $choice;
})
.click();
@ -768,7 +768,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=non-string-values]')
.find('.choices__list--single .choices__item')
.last()
.should($item => {
.should(($item) => {
expect($item.text().trim()).to.equal($selectedChoice.text().trim());
});
});
@ -778,7 +778,7 @@ describe('Choices - select multiple', () => {
describe('selecting choice', () => {
describe('on enter key', () => {
it('selects choice', () => {
cy.get('[data-test-hook=within-form] form').then($form => {
cy.get('[data-test-hook=within-form] form').then(($form) => {
$form.submit(() => {
// this will fail the test if the form submits
throw new Error('Form submitted');
@ -793,7 +793,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=within-form]')
.find('.choices__list--multiple .choices__item')
.last()
.should($item => {
.should(($item) => {
expect($item).to.contain('Choice 1');
});
});
@ -808,7 +808,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=set-choice-by-value]')
.find('.choices__list--multiple .choices__item')
.last()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal(
dynamicallySelectedChoiceValue,
);
@ -819,7 +819,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=set-choice-by-value]')
.find('.choices__list--dropdown .choices__list')
.children()
.each($choice => {
.each(($choice) => {
expect($choice.text().trim()).to.not.equal(
dynamicallySelectedChoiceValue,
);
@ -829,7 +829,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[hidden]')
.should($select => {
.should(($select) => {
const val = $select.val() || [];
expect(val).to.contain(dynamicallySelectedChoiceValue);
});
@ -846,7 +846,7 @@ describe('Choices - select multiple', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal('No results found');
});
});
@ -860,7 +860,7 @@ describe('Choices - select multiple', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal('label1');
});
});

31701
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@
"cypress:open": "cypress open",
"cypress:ci": "cypress run --record --group $GITHUB_REF --ci-build-id $GITHUB_SHA",
"test": "run-s test:unit test:e2e",
"test:unit": "TS_NODE_TRANSPILE_ONLY=true NODE_ENV=test mocha",
"test:unit": "cross-env TS_NODE_TRANSPILE_ONLY=true NODE_ENV=test mocha",
"test:unit:watch": "npm run test:unit -- --watch --inspect=5556",
"test:unit:coverage": "NODE_ENV=test nyc --reporter=lcov --reporter=text --reporter=text-summary mocha",
"test:e2e": "run-p --race start cypress:run",
@ -53,56 +53,57 @@
"js"
],
"devDependencies": {
"@babel/core": "^7.6.4",
"@babel/preset-env": "^7.6.3",
"@babel/register": "^7.6.2",
"@types/chai": "^4.2.7",
"@types/mocha": "^5.2.7",
"@types/sinon": "^7.5.1",
"@types/sinon-chai": "^3.2.3",
"@typescript-eslint/eslint-plugin": "^2.11.0",
"@typescript-eslint/parser": "^2.11.0",
"autoprefixer": "^9.6.5",
"babel-loader": "^8.0.6",
"bundlesize": "^0.18.0",
"chai": "^4.2.0",
"cross-env": "^6.0.3",
"@babel/core": "^7.16.5",
"@babel/preset-env": "^7.16.5",
"@babel/register": "^7.16.5",
"@types/chai": "^4.3.0",
"@types/mocha": "^9.0.0",
"@types/sinon": "^10.0.6",
"@types/sinon-chai": "^3.2.6",
"@typescript-eslint/eslint-plugin": "^5.7.0",
"@typescript-eslint/parser": "^5.7.0",
"autoprefixer": "^10.4.0",
"babel-loader": "^8.2.3",
"bundlesize": "^0.18.1",
"chai": "^4.3.4",
"cross-env": "^7.0.3",
"csso-cli": "^3.0.0",
"cypress": "3.6.0",
"eslint": "^6.8.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-config-prettier": "^6.5.0",
"cypress": "9.1.1",
"eslint": "^8.4.1",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^16.1.0",
"eslint-config-prettier": "^8.3.0",
"eslint-loader": "^3.0.2",
"eslint-plugin-compat": "3.3.0",
"eslint-plugin-cypress": "^2.8.1",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-sort-class-members": "^1.6.0",
"express": "^4.16.4",
"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",
"eslint-plugin-compat": "4.0.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-sort-class-members": "^1.14.1",
"express": "^4.17.1",
"husky": "^7.0.4",
"jsdom": "^19.0.0",
"lint-staged": "^12.1.2",
"mocha": "^9.1.3",
"node-sass": "^7.0.0",
"nodemon": "^2.0.15",
"npm-run-all": "^4.1.5",
"nyc": "^14.1.1",
"postcss-cli": "^6.1.3",
"prettier": "^1.19.1",
"sinon": "^7.5.0",
"sinon-chai": "^3.3.0",
"ts-loader": "^6.2.1",
"ts-node": "^8.5.4",
"typescript": "^3.7.3",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.9",
"webpack-dev-middleware": "^3.7.2",
"webpack-hot-middleware": "^2.25.0"
"nyc": "^15.1.0",
"postcss-cli": "^9.1.0",
"prettier": "^2.5.1",
"sinon": "^12.0.1",
"sinon-chai": "^3.7.0",
"ts-loader": "^9.2.6",
"ts-node": "^10.4.0",
"typescript": "^4.5.4",
"webpack": "^5.65.0",
"webpack-cli": "^4.9.1",
"webpack-dev-middleware": "^5.2.2",
"webpack-hot-middleware": "^2.25.1"
},
"dependencies": {
"deepmerge": "^4.2.0",
"deepmerge": "^4.2.2",
"fuse.js": "^3.4.6",
"redux": "^4.0.4"
"redux": "^4.1.2"
},
"npmName": "choices.js",
"npmFileMap": [
@ -125,7 +126,7 @@
"bundlesize": [
{
"path": "public/assets/scripts/choices.min.js",
"maxSize": "20 kB"
"maxSize": "25 kB"
},
{
"path": "public/assets/styles/choices.min.css",

View File

@ -78,7 +78,6 @@ a:focus {
border-radius: 2.5px;
font-size: 14px;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
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}[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;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

@ -25,7 +25,6 @@
background-color: #eaeaea;
cursor: not-allowed;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
@ -320,7 +319,6 @@
.choices__item--disabled {
cursor: not-allowed;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
opacity: 0.5;
}
@ -336,7 +334,6 @@
.choices__button {
text-indent: -9999px;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border: 0;
background-color: transparent;

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
import { ACTION_TYPES } from '../constants';
import { Choice } from '../interfaces';
import { Choice } from '../interfaces/choice';
export interface AddChoiceAction {
type: typeof ACTION_TYPES.ADD_CHOICE;

View File

@ -1,6 +1,6 @@
import { expect } from 'chai';
import { State } from '../interfaces/state';
import * as actions from './misc';
import { State } from '../interfaces';
describe('actions/misc', () => {
describe('clearAll action', () => {

View File

@ -1,5 +1,5 @@
import { State } from '../interfaces';
import { ACTION_TYPES } from '../constants';
import { State } from '../interfaces/state';
export interface ClearAllAction {
type: typeof ACTION_TYPES.CLEAR_ALL;

View File

@ -4,11 +4,14 @@ import sinonChai from 'sinon-chai';
import Choices from './choices';
import { EVENTS, ACTION_TYPES, DEFAULT_CONFIG, KEY_CODES } from './constants';
import { EVENTS, ACTION_TYPES, KEY_CODES } from './constants';
import { WrappedSelect, WrappedInput } from './components/index';
import { removeItem } from './actions/items';
import { Item, Choice, Group } from './interfaces';
import templates from './templates';
import { Choice } from './interfaces/choice';
import { Group } from './interfaces/group';
import { Item } from './interfaces/item';
import { DEFAULT_CONFIG } from './defaults';
chai.use(sinonChai);
@ -563,21 +566,21 @@ describe('choices', () => {
expect(output).to.eql(instance);
});
it('opens containerOuter', done => {
it('opens containerOuter', (done) => {
requestAnimationFrame(() => {
expect(containerOuterOpenSpy.called).to.equal(true);
done();
});
});
it('shows dropdown with blurInput flag', done => {
it('shows dropdown with blurInput flag', (done) => {
requestAnimationFrame(() => {
expect(dropdownShowSpy.called).to.equal(true);
done();
});
});
it('triggers event on passedElement', done => {
it('triggers event on passedElement', (done) => {
requestAnimationFrame(() => {
expect(passedElementTriggerEventStub.called).to.equal(true);
expect(passedElementTriggerEventStub.lastCall.args[0]).to.eql(
@ -595,7 +598,7 @@ describe('choices', () => {
output = instance.showDropdown(true);
});
it('focuses input', done => {
it('focuses input', (done) => {
requestAnimationFrame(() => {
expect(inputFocusSpy.called).to.equal(true);
done();
@ -661,21 +664,21 @@ describe('choices', () => {
expect(output).to.eql(instance);
});
it('closes containerOuter', done => {
it('closes containerOuter', (done) => {
requestAnimationFrame(() => {
expect(containerOuterCloseSpy.called).to.equal(true);
done();
});
});
it('hides dropdown with blurInput flag', done => {
it('hides dropdown with blurInput flag', (done) => {
requestAnimationFrame(() => {
expect(dropdownHideSpy.called).to.equal(true);
done();
});
});
it('triggers event on passedElement', done => {
it('triggers event on passedElement', (done) => {
requestAnimationFrame(() => {
expect(passedElementTriggerEventStub.called).to.equal(true);
expect(passedElementTriggerEventStub.lastCall.args[0]).to.eql(
@ -693,14 +696,14 @@ describe('choices', () => {
output = instance.hideDropdown(true);
});
it('removes active descendants', done => {
it('removes active descendants', (done) => {
requestAnimationFrame(() => {
expect(inputRemoveActiveDescendantSpy.called).to.equal(true);
done();
});
});
it('blurs input', done => {
it('blurs input', (done) => {
requestAnimationFrame(() => {
expect(inputBlurSpy.called).to.equal(true);
done();
@ -1192,7 +1195,8 @@ describe('choices', () => {
const fetcher = async (inst): Promise<Choice[]> => {
expect(inst).to.eq(choice);
fetcherCalled = true;
await new Promise(resolve => setTimeout(resolve, 800));
// eslint-disable-next-line no-promise-executor-return
await new Promise((resolve) => setTimeout(resolve, 800));
return [
{ label: 'l1', value: 'v1', customProperties: { prop1: true } },
@ -1381,7 +1385,7 @@ describe('choices', () => {
});
it('returns all active item values', () => {
expect(output).to.eql(items.map(item => item.value));
expect(output).to.eql(items.map((item) => item.value));
});
});
});
@ -1612,7 +1616,8 @@ describe('choices', () => {
instance.clearChoices = clearChoicesStub;
instance._addGroup = addGroupStub;
instance._addChoice = addChoiceStub;
instance.containerOuter.removeLoadingState = containerOuterRemoveLoadingStateStub;
instance.containerOuter.removeLoadingState =
containerOuterRemoveLoadingStateStub;
});
afterEach(() => {
@ -2093,7 +2098,7 @@ describe('choices', () => {
KEY_CODES.PAGE_DOWN_KEY,
];
keyCodes.forEach(keyCode => {
keyCodes.forEach((keyCode) => {
it(`calls _onDirectionKey with the expected arguments`, () => {
const event = {
keyCode,
@ -2143,7 +2148,7 @@ describe('choices', () => {
describe('delete key', () => {
const keyCodes = [KEY_CODES.DELETE_KEY, KEY_CODES.BACK_KEY];
keyCodes.forEach(keyCode => {
keyCodes.forEach((keyCode) => {
it(`calls _onDeleteKey with the expected arguments`, () => {
const event = {
keyCode,
@ -2188,10 +2193,10 @@ describe('choices', () => {
);
});
it('triggers a REMOVE_ITEM event on the passed element', done => {
it('triggers a REMOVE_ITEM event on the passed element', (done) => {
passedElement.addEventListener(
'removeItem',
event => {
(event) => {
expect(event.detail).to.eql({
id: item.id,
value: item.value,
@ -2226,10 +2231,10 @@ describe('choices', () => {
instance._store.getGroupById.reset();
});
it("includes the group's value in the triggered event", done => {
it("includes the group's value in the triggered event", (done) => {
passedElement.addEventListener(
'removeItem',
event => {
(event) => {
expect(event.detail).to.eql({
id: itemWithGroup.id,
value: itemWithGroup.value,

View File

@ -1,56 +1,55 @@
import merge from 'deepmerge';
/* eslint-disable @typescript-eslint/no-explicit-any */
import Fuse from 'fuse.js';
import merge from 'deepmerge';
import Store from './store/store';
import {
Dropdown,
activateChoices,
addChoice,
clearChoices,
filterChoices,
Result,
} from './actions/choices';
import { addGroup } from './actions/groups';
import { addItem, highlightItem, removeItem } from './actions/items';
import { clearAll, resetTo, setIsLoading } from './actions/misc';
import {
Container,
Dropdown,
Input,
List,
WrappedInput,
WrappedSelect,
} from './components';
import {
DEFAULT_CONFIG,
EVENTS,
KEY_CODES,
TEXT_TYPE,
SELECT_ONE_TYPE,
SELECT_MULTIPLE_TYPE,
SELECT_ONE_TYPE,
TEXT_TYPE,
} from './constants';
import templates from './templates';
import { DEFAULT_CONFIG } from './defaults';
import { Choice } from './interfaces/choice';
import { Group } from './interfaces/group';
import { Item } from './interfaces/item';
import { Notice } from './interfaces/notice';
import { Options } from './interfaces/options';
import { PassedElement } from './interfaces/passed-element';
import { State } from './interfaces/state';
import {
addChoice,
filterChoices,
activateChoices,
clearChoices,
Result,
} from './actions/choices';
import { addItem, removeItem, highlightItem } from './actions/items';
import { addGroup } from './actions/groups';
import { clearAll, resetTo, setIsLoading } from './actions/misc';
import {
isScrolledIntoView,
diff,
existsInArray,
generateId,
getAdjacentEl,
getType,
isScrolledIntoView,
isType,
strToEl,
sortByScore,
generateId,
existsInArray,
diff,
strToEl,
} from './lib/utils';
import {
Options,
Choice,
Item,
Group,
Notice,
State,
PassedElement,
} from './interfaces';
import { defaultState } from './reducers';
import Store from './store/store';
import templates from './templates';
/** @see {@link http://browserhacks.com/#hack-acea075d0ac6954f275a70023906050c} */
const IS_IE11 =
@ -63,7 +62,7 @@ const USER_DEFAULTS: Partial<Options> = {};
* Choices
* @author Josh Johnson<josh@joshuajohnson.co.uk>
*/
class Choices {
class Choices implements Choices {
static get defaults(): {
options: Partial<Options>;
templates: typeof templates;
@ -79,39 +78,69 @@ class Choices {
}
initialised: boolean;
config: Options;
passedElement: WrappedInput | WrappedSelect;
containerOuter: Container;
containerInner: Container;
choiceList: List;
itemList: List;
input: Input;
dropdown: Dropdown;
_isTextElement: boolean;
_isSelectOneElement: boolean;
_isSelectMultipleElement: boolean;
_isSelectElement: boolean;
_store: Store;
_templates: typeof templates;
_initialState: State;
_currentState: State;
_prevState: State;
_currentValue: string;
_canSearch: boolean;
_isScrollingOnIe: boolean;
_highlightPosition: number;
_wasTap: boolean;
_isSearching: boolean;
_placeholderValue: string | null;
_baseId: string;
_direction: HTMLElement['dir'];
_idNames: {
itemChoice: string;
};
_presetGroups: Group[] | HTMLOptGroupElement[] | Element[];
_presetOptions: Item[] | HTMLOptionElement[];
_presetChoices: Partial<Choice>[];
_presetItems: Item[] | string[];
constructor(
@ -247,7 +276,7 @@ class Choices {
}
// Create array of choices from option elements
if ((this.passedElement as WrappedSelect).options) {
(this.passedElement as WrappedSelect).options.forEach(option => {
(this.passedElement as WrappedSelect).options.forEach((option) => {
this._presetChoices.push({
value: option.value,
label: option.innerHTML,
@ -415,21 +444,21 @@ class Choices {
}
highlightAll(): this {
this._store.items.forEach(item => this.highlightItem(item));
this._store.items.forEach((item) => this.highlightItem(item));
return this;
}
unhighlightAll(): this {
this._store.items.forEach(item => this.unhighlightItem(item));
this._store.items.forEach((item) => this.unhighlightItem(item));
return this;
}
removeActiveItemsByValue(value: string): this {
this._store.activeItems
.filter(item => item.value === value)
.forEach(item => this._removeItem(item));
.filter((item) => item.value === value)
.forEach((item) => this._removeItem(item));
return this;
}
@ -437,13 +466,13 @@ class Choices {
removeActiveItems(excludedId: number): this {
this._store.activeItems
.filter(({ id }) => id !== excludedId)
.forEach(item => this._removeItem(item));
.forEach((item) => this._removeItem(item));
return this;
}
removeHighlightedItems(runEvent = false): this {
this._store.highlightedActiveItems.forEach(item => {
this._store.highlightedActiveItems.forEach((item) => {
this._removeItem(item);
// If this action was performed by the user
// trigger the event
@ -513,7 +542,7 @@ class Choices {
return this;
}
items.forEach(value => this._setChoiceOrItem(value));
items.forEach((value) => this._setChoiceOrItem(value));
return this;
}
@ -527,7 +556,7 @@ class Choices {
const choiceValue = Array.isArray(value) ? value : [value];
// Loop through each value and
choiceValue.forEach(val => this._findAndSelectChoiceByValue(val));
choiceValue.forEach((val) => this._findAndSelectChoiceByValue(val));
return this;
}
@ -630,14 +659,14 @@ class Choices {
if (typeof Promise === 'function' && fetcher instanceof Promise) {
// that's a promise
// eslint-disable-next-line compat/compat
return new Promise(resolve => requestAnimationFrame(resolve)) // eslint-disable-line compat/compat
// eslint-disable-next-line no-promise-executor-return
return new Promise((resolve) => requestAnimationFrame(resolve))
.then(() => this._handleLoadingState(true))
.then(() => fetcher)
.then((data: Choice[]) =>
this.setChoices(data, value, label, replaceChoices),
)
.catch(err => {
.catch((err) => {
if (!this.config.silent) {
console.error(err);
}
@ -766,7 +795,7 @@ class Choices {
if (activeGroups.length >= 1 && !this._isSearching) {
// If we have a placeholder choice along with groups
const activePlaceholders = activeChoices.filter(
activeChoice =>
(activeChoice) =>
activeChoice.placeholder === true && activeChoice.groupId === -1,
);
if (activePlaceholders.length >= 1) {
@ -849,7 +878,7 @@ class Choices {
fragment: DocumentFragment = document.createDocumentFragment(),
): DocumentFragment {
const getGroupChoices = (group): Choice[] =>
choices.filter(choice => {
choices.filter((choice) => {
if (this._isSelectOneElement) {
return choice.groupId === group.id;
}
@ -865,7 +894,7 @@ class Choices {
groups.sort(this.config.sorter);
}
groups.forEach(group => {
groups.forEach((group) => {
const groupChoices = getGroupChoices(group);
if (groupChoices.length >= 1) {
const dropdownGroup = this._getTemplate('choiceGroup', group);
@ -883,11 +912,8 @@ class Choices {
withinGroup = false,
): DocumentFragment {
// Create a fragment to store our list items (so we don't have to update the DOM for each item)
const {
renderSelectedChoices,
searchResultLimit,
renderChoiceLimit,
} = this.config;
const { renderSelectedChoices, searchResultLimit, renderChoiceLimit } =
this.config;
const filter = this._isSearching ? sortByScore : this.config.sorter;
const appendChoice = (choice: Choice): void => {
const shouldRender =
@ -909,7 +935,7 @@ class Choices {
let rendererableChoices = choices;
if (renderSelectedChoices === 'auto' && !this._isSelectOneElement) {
rendererableChoices = choices.filter(choice => !choice.selected);
rendererableChoices = choices.filter((choice) => !choice.selected);
}
// Split array into placeholders and "normal" choices
@ -1027,7 +1053,7 @@ class Choices {
const itemId =
element.parentNode && (element.parentNode as HTMLElement).dataset.id;
const itemToRemove =
itemId && activeItems.find(item => item.id === parseInt(itemId, 10));
itemId && activeItems.find((item) => item.id === parseInt(itemId, 10));
if (!itemToRemove) {
return;
@ -1061,7 +1087,7 @@ class Choices {
// We only want to select one item with a click
// so we deselect any items that aren't the target
// unless shift is being pressed
activeItems.forEach(item => {
activeItems.forEach((item) => {
if (item.id === parseInt(`${passedId}`, 10) && !item.highlighted) {
this.highlightItem(item);
} else if (!hasShiftKey && item.highlighted) {
@ -1132,7 +1158,7 @@ class Choices {
}
const lastItem = activeItems[activeItems.length - 1];
const hasHighlightedItems = activeItems.some(item => item.highlighted);
const hasHighlightedItems = activeItems.some((item) => item.highlighted);
// If editing the last item is allowed and there are not other selected items,
// we can edit the item value. Otherwise if we can remove items, remove all selected items
@ -1204,7 +1230,7 @@ class Choices {
const { choices } = this._store;
const { searchFloor, searchChoices } = this.config;
const hasUnactiveChoices = choices.some(option => !option.active);
const hasUnactiveChoices = choices.some((option) => !option.active);
// Check that we have a value to search and the input was an alphanumeric character
if (value && value.length >= searchFloor) {
@ -1800,7 +1826,7 @@ class Choices {
if (blurWasWithinContainer && !this._isScrollingOnIe) {
const { activeItems } = this._store;
const hasHighlightedItems = activeItems.some(item => item.highlighted);
const hasHighlightedItems = activeItems.some((item) => item.highlighted);
const blurActions = {
[TEXT_TYPE]: (): void => {
if (target === this.input.element) {
@ -1862,7 +1888,7 @@ class Choices {
);
// Remove any highlighted choices
highlightedChoices.forEach(choice => {
highlightedChoices.forEach((choice) => {
choice.classList.remove(this.config.classNames.highlightedState);
choice.setAttribute('aria-selected', 'false');
});
@ -2214,7 +2240,7 @@ class Choices {
});
}
groups.forEach(group =>
groups.forEach((group) =>
this._addGroup({
group,
id: group.id || null,
@ -2228,9 +2254,9 @@ class Choices {
choices.sort(this.config.sorter);
}
const hasSelectedChoice = choices.some(choice => choice.selected);
const hasSelectedChoice = choices.some((choice) => choice.selected);
const firstEnabledChoiceIndex = choices.findIndex(
choice => choice.disabled === undefined || !choice.disabled,
(choice) => choice.disabled === undefined || !choice.disabled,
);
choices.forEach((choice, index) => {
@ -2258,8 +2284,6 @@ class Choices {
const isSelected = shouldPreselect ? true : choice.selected;
const isDisabled = choice.disabled;
console.log(isDisabled, choice);
this._addChoice({
value,
label,
@ -2283,7 +2307,7 @@ class Choices {
}
_addPredefinedItems(items: Item[] | string[]): void {
items.forEach(item => {
items.forEach((item) => {
if (typeof item === 'object' && item.value) {
this._addItem({
value: item.value,
@ -2353,7 +2377,7 @@ class Choices {
_findAndSelectChoiceByValue(value: string): void {
const { choices } = this._store;
// Check 'value' property exists and the choice isn't already selected
const foundChoice = choices.find(choice =>
const foundChoice = choices.find((choice) =>
this.config.valueComparer(choice.value, value),
);

View File

@ -1,7 +1,7 @@
import { expect } from 'chai';
import { stub } from 'sinon';
import { DEFAULT_CLASSNAMES } from '../defaults';
import Container from './container';
import { DEFAULT_CLASSNAMES } from '../constants';
describe('components/container', () => {
let instance;

View File

@ -1,16 +1,26 @@
import { wrap } from '../lib/utils';
import { SELECT_ONE_TYPE } from '../constants';
import { PassedElement, ClassNames, Options } from '../interfaces';
import { ClassNames } from '../interfaces/class-names';
import { PositionOptionsType } from '../interfaces/position-options-type';
import { PassedElementType } from '../interfaces/passed-element-type';
export default class Container {
element: HTMLElement;
type: PassedElement['type'];
type: PassedElementType;
classNames: ClassNames;
position: Options['position'];
position: PositionOptionsType;
isOpen: boolean;
isFlipped: boolean;
isFocussed: boolean;
isDisabled: boolean;
isLoading: boolean;
constructor({
@ -20,9 +30,9 @@ export default class Container {
position,
}: {
element: HTMLElement;
type: PassedElement['type'];
type: PassedElementType;
classNames: ClassNames;
position: Options['position'];
position: PositionOptionsType;
}) {
this.element = element;
this.classNames = classNames;

View File

@ -1,7 +1,7 @@
import { expect } from 'chai';
import sinon from 'sinon';
import { DEFAULT_CLASSNAMES } from '../defaults';
import Dropdown from './dropdown';
import { DEFAULT_CLASSNAMES } from '../constants';
describe('components/dropdown', () => {
let instance;

View File

@ -1,9 +1,13 @@
import { PassedElement, ClassNames } from '../interfaces';
import { ClassNames } from '../interfaces/class-names';
import { PassedElementType } from '../interfaces/passed-element-type';
export default class Dropdown {
element: HTMLElement;
type: PassedElement['type'];
type: PassedElementType;
classNames: ClassNames;
isActive: boolean;
constructor({
@ -12,7 +16,7 @@ export default class Dropdown {
classNames,
}: {
element: HTMLElement;
type: PassedElement['type'];
type: PassedElementType;
classNames: ClassNames;
}) {
this.element = element;

View File

@ -1,7 +1,7 @@
import { expect } from 'chai';
import { stub } from 'sinon';
import { DEFAULT_CLASSNAMES } from '../defaults';
import Input from './input';
import { DEFAULT_CLASSNAMES } from '../constants';
describe('components/input', () => {
let instance;

View File

@ -1,13 +1,19 @@
import { sanitise } from '../lib/utils';
import { SELECT_ONE_TYPE } from '../constants';
import { PassedElement, ClassNames } from '../interfaces';
import { ClassNames } from '../interfaces/class-names';
import { PassedElementType } from '../interfaces/passed-element-type';
export default class Input {
element: HTMLInputElement;
type: PassedElement['type'];
type: PassedElementType;
classNames: ClassNames;
preventPaste: boolean;
isFocussed: boolean;
isDisabled: boolean;
constructor({
@ -17,7 +23,7 @@ export default class Input {
preventPaste,
}: {
element: HTMLInputElement;
type: PassedElement['type'];
type: PassedElementType;
classNames: ClassNames;
preventPaste: boolean;
}) {

View File

@ -2,7 +2,9 @@ import { SCROLLING_SPEED } from '../constants';
export default class List {
element: HTMLElement;
scrollPos: number;
height: number;
constructor({ element }: { element: HTMLElement }) {

View File

@ -1,6 +1,6 @@
import { expect } from 'chai';
import { DEFAULT_CLASSNAMES } from '../defaults';
import WrappedElement from './wrapped-element';
import { DEFAULT_CLASSNAMES } from '../constants';
describe('components/wrappedElement', () => {
let instance;
@ -163,7 +163,7 @@ describe('components/wrappedElement', () => {
});
describe('triggerEvent', () => {
it('fires event on element using passed eventType and data', done => {
it('fires event on element using passed eventType and data', (done) => {
const data = {
test: true,
};

View File

@ -1,9 +1,12 @@
import { ClassNames } from '../interfaces/class-names';
import { EventType } from '../interfaces/event-type';
import { dispatchEvent } from '../lib/utils';
import { ClassNames, EventMap } from '../interfaces';
export default class WrappedElement {
element: HTMLInputElement | HTMLSelectElement;
classNames: ClassNames;
isDisabled: boolean;
constructor({ element, classNames }) {
@ -89,7 +92,7 @@ export default class WrappedElement {
this.isDisabled = true;
}
triggerEvent<K extends keyof EventMap>(eventType: K, data?: object): void {
triggerEvent(eventType: EventType, data?: object): void {
dispatchEvent(this.element, eventType, data);
}
}

View File

@ -1,8 +1,8 @@
import { expect } from 'chai';
import { stub } from 'sinon';
import { DEFAULT_CLASSNAMES } from '../defaults';
import WrappedElement from './wrapped-element';
import WrappedInput from './wrapped-input';
import { DEFAULT_CLASSNAMES } from '../constants';
describe('components/wrappedInput', () => {
let instance;
@ -36,7 +36,7 @@ describe('components/wrappedInput', () => {
describe('inherited methods', () => {
const methods: string[] = ['conceal', 'reveal', 'enable', 'disable'];
methods.forEach(method => {
methods.forEach((method) => {
describe(method, () => {
beforeEach(() => {
stub(WrappedElement.prototype, method as keyof WrappedElement);

View File

@ -1,8 +1,9 @@
import { ClassNames } from '../interfaces/class-names';
import WrappedElement from './wrapped-element';
import { ClassNames } from '../interfaces';
export default class WrappedInput extends WrappedElement {
element: HTMLInputElement;
delimiter: string;
constructor({

View File

@ -2,8 +2,8 @@ import { expect } from 'chai';
import { stub, spy } from 'sinon';
import WrappedElement from './wrapped-element';
import WrappedSelect from './wrapped-select';
import { DEFAULT_CLASSNAMES } from '../constants';
import Templates from '../templates';
import { DEFAULT_CLASSNAMES } from '../defaults';
describe('components/wrappedSelect', () => {
let instance;
@ -56,7 +56,7 @@ describe('components/wrappedSelect', () => {
describe('inherited methods', () => {
const methods: string[] = ['conceal', 'reveal', 'enable', 'disable'];
methods.forEach(method => {
methods.forEach((method) => {
beforeEach(() => {
stub(WrappedElement.prototype, method as keyof WrappedElement);
});
@ -93,7 +93,7 @@ describe('components/wrappedSelect', () => {
it('returns all option elements', () => {
const { options } = instance;
expect(options).to.be.an('array');
options.forEach(option => {
options.forEach((option) => {
expect(option).to.be.instanceOf(HTMLOptionElement);
});
});
@ -108,7 +108,7 @@ describe('components/wrappedSelect', () => {
const { optionGroups } = instance;
expect(optionGroups.length).to.equal(3);
optionGroups.forEach(option => {
optionGroups.forEach((option) => {
expect(option).to.be.instanceOf(HTMLOptGroupElement);
});
});

View File

@ -1,9 +1,12 @@
import { ClassNames } from '../interfaces/class-names';
import { Item } from '../interfaces/item';
import WrappedElement from './wrapped-element';
import { ClassNames, Item } from '../interfaces';
export default class WrappedSelect extends WrappedElement {
element: HTMLSelectElement;
classNames: ClassNames;
template: (data: object) => HTMLOptionElement;
constructor({
@ -45,7 +48,7 @@ export default class WrappedSelect extends WrappedElement {
};
// Add each list item to list
options.forEach(optionData => addOptionToFragment(optionData));
options.forEach((optionData) => addOptionToFragment(optionData));
this.appendDocFragment(fragment);
}

View File

@ -1,12 +1,6 @@
import { expect } from 'chai';
import {
DEFAULT_CLASSNAMES,
DEFAULT_CONFIG,
EVENTS,
ACTION_TYPES,
KEY_CODES,
SCROLLING_SPEED,
} from './constants';
import { EVENTS, ACTION_TYPES, KEY_CODES, SCROLLING_SPEED } from './constants';
import { DEFAULT_CLASSNAMES, DEFAULT_CONFIG } from './defaults';
describe('constants', () => {
describe('type checks', () => {
@ -145,7 +139,7 @@ describe('constants', () => {
});
it('exports each value as a number', () => {
Object.keys(KEY_CODES).forEach(key => {
Object.keys(KEY_CODES).forEach((key) => {
expect(KEY_CODES[key]).to.be.a('number');
});
});

View File

@ -1,89 +1,8 @@
import { sanitise, sortByAlpha } from './lib/utils';
import {
Options,
ClassNames,
EventMap,
ActionType,
KeyCodeMap,
} from './interfaces';
import { ActionType } from './interfaces/action-type';
import { EventType } from './interfaces/event-type';
import { KeyCodeMap } from './interfaces/keycode-map';
export const DEFAULT_CLASSNAMES: ClassNames = {
containerOuter: 'choices',
containerInner: 'choices__inner',
input: 'choices__input',
inputCloned: 'choices__input--cloned',
list: 'choices__list',
listItems: 'choices__list--multiple',
listSingle: 'choices__list--single',
listDropdown: 'choices__list--dropdown',
item: 'choices__item',
itemSelectable: 'choices__item--selectable',
itemDisabled: 'choices__item--disabled',
itemChoice: 'choices__item--choice',
placeholder: 'choices__placeholder',
group: 'choices__group',
groupHeading: 'choices__heading',
button: 'choices__button',
activeState: 'is-active',
focusState: 'is-focused',
openState: 'is-open',
disabledState: 'is-disabled',
highlightedState: 'is-highlighted',
selectedState: 'is-selected',
flippedState: 'is-flipped',
loadingState: 'is-loading',
noResults: 'has-no-results',
noChoices: 'has-no-choices',
};
export const DEFAULT_CONFIG: Options = {
items: [],
choices: [],
silent: false,
renderChoiceLimit: -1,
maxItemCount: -1,
addItems: true,
addItemFilter: null,
removeItems: true,
removeItemButton: false,
editItems: false,
duplicateItemsAllowed: true,
delimiter: ',',
paste: true,
searchEnabled: true,
searchChoices: true,
searchFloor: 1,
searchResultLimit: 4,
searchFields: ['label', 'value'],
position: 'auto',
resetScrollPosition: true,
shouldSort: true,
shouldSortItems: false,
sorter: sortByAlpha,
placeholder: true,
placeholderValue: null,
searchPlaceholderValue: null,
prependValue: null,
appendValue: null,
renderSelectedChoices: 'auto',
loadingText: 'Loading...',
noResultsText: 'No results found',
noChoicesText: 'No choices to choose from',
itemSelectText: 'Press to select',
uniqueItemText: 'Only unique values can be added',
customAddItemText: 'Only values matching specific conditions can be added',
addItemText: value => `Press Enter to add <b>"${sanitise(value)}"</b>`,
maxItemText: maxItemCount => `Only ${maxItemCount} values can be added`,
valueComparer: (value1, value2) => value1 === value2,
fuseOptions: {
includeScore: true,
},
callbackOnInit: null,
callbackOnCreateTemplates: null,
classNames: DEFAULT_CLASSNAMES,
};
export const EVENTS: Record<keyof EventMap, keyof EventMap> = {
export const EVENTS: Record<EventType, EventType> = {
showDropdown: 'showDropdown',
hideDropdown: 'hideDropdown',
change: 'change',

79
src/scripts/defaults.ts Normal file
View File

@ -0,0 +1,79 @@
import { ClassNames } from './interfaces/class-names';
import { Options } from './interfaces/options';
import { sortByAlpha, sanitise } from './lib/utils';
export const DEFAULT_CLASSNAMES: ClassNames = {
containerOuter: 'choices',
containerInner: 'choices__inner',
input: 'choices__input',
inputCloned: 'choices__input--cloned',
list: 'choices__list',
listItems: 'choices__list--multiple',
listSingle: 'choices__list--single',
listDropdown: 'choices__list--dropdown',
item: 'choices__item',
itemSelectable: 'choices__item--selectable',
itemDisabled: 'choices__item--disabled',
itemChoice: 'choices__item--choice',
placeholder: 'choices__placeholder',
group: 'choices__group',
groupHeading: 'choices__heading',
button: 'choices__button',
activeState: 'is-active',
focusState: 'is-focused',
openState: 'is-open',
disabledState: 'is-disabled',
highlightedState: 'is-highlighted',
selectedState: 'is-selected',
flippedState: 'is-flipped',
loadingState: 'is-loading',
noResults: 'has-no-results',
noChoices: 'has-no-choices',
};
export const DEFAULT_CONFIG: Options = {
items: [],
choices: [],
silent: false,
renderChoiceLimit: -1,
maxItemCount: -1,
addItems: true,
addItemFilter: null,
removeItems: true,
removeItemButton: false,
editItems: false,
duplicateItemsAllowed: true,
delimiter: ',',
paste: true,
searchEnabled: true,
searchChoices: true,
searchFloor: 1,
searchResultLimit: 4,
searchFields: ['label', 'value'],
position: 'auto',
resetScrollPosition: true,
shouldSort: true,
shouldSortItems: false,
sorter: sortByAlpha,
placeholder: true,
placeholderValue: null,
searchPlaceholderValue: null,
prependValue: null,
appendValue: null,
renderSelectedChoices: 'auto',
loadingText: 'Loading...',
noResultsText: 'No results found',
noChoicesText: 'No choices to choose from',
itemSelectText: 'Press to select',
uniqueItemText: 'Only unique values can be added',
customAddItemText: 'Only values matching specific conditions can be added',
addItemText: (value) => `Press Enter to add <b>"${sanitise(value)}"</b>`,
maxItemText: (maxItemCount) => `Only ${maxItemCount} values can be added`,
valueComparer: (value1, value2) => value1 === value2,
fuseOptions: {
includeScore: true,
},
callbackOnInit: null,
callbackOnCreateTemplates: null,
classNames: DEFAULT_CLASSNAMES,
};

View File

@ -0,0 +1,12 @@
export type ActionType =
| 'ADD_CHOICE'
| 'FILTER_CHOICES'
| 'ACTIVATE_CHOICES'
| 'CLEAR_CHOICES'
| 'ADD_GROUP'
| 'ADD_ITEM'
| 'REMOVE_ITEM'
| 'HIGHLIGHT_ITEM'
| 'CLEAR_ALL'
| 'RESET_TO'
| 'SET_IS_LOADING';

View File

@ -0,0 +1,17 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export interface Choice {
id?: number;
customProperties?: Record<string, any>;
disabled?: boolean;
active?: boolean;
elementId?: number;
groupId?: number;
keyCode?: number;
label: string;
placeholder?: boolean;
selected?: boolean;
value: string;
score?: number;
choices?: Choice[];
}

View File

@ -0,0 +1,87 @@
import { Options } from 'deepmerge';
import { Store } from 'redux';
import {
WrappedInput,
WrappedSelect,
Container,
List,
Input,
Dropdown,
} from '../components';
import { Choice } from './choice';
import { Group } from './group';
import { Item } from './item';
import { State } from './state';
import templates from '../templates';
export interface Choices {
initialised: boolean;
config: Options;
passedElement: WrappedInput | WrappedSelect;
containerOuter: Container;
containerInner: Container;
choiceList: List;
itemList: List;
input: Input;
dropdown: Dropdown;
_isTextElement: boolean;
_isSelectOneElement: boolean;
_isSelectMultipleElement: boolean;
_isSelectElement: boolean;
_store: Store;
_templates: typeof templates;
_initialState: State;
_currentState: State;
_prevState: State;
_currentValue: string;
_canSearch: boolean;
_isScrollingOnIe: boolean;
_highlightPosition: number;
_wasTap: boolean;
_isSearching: boolean;
_placeholderValue: string | null;
_baseId: string;
_direction: HTMLElement['dir'];
_idNames: {
itemChoice: string;
};
_presetGroups: Group[] | HTMLOptGroupElement[] | Element[];
_presetOptions: Item[] | HTMLOptionElement[];
_presetChoices: Partial<Choice>[];
_presetItems: Item[] | string[];
new (
element: string | Element | HTMLInputElement | HTMLSelectElement,
userConfig: Partial<Options>,
);
}

View File

@ -0,0 +1,55 @@
/** Classes added to HTML generated by By default classnames follow the BEM notation. */
export interface ClassNames {
/** @default 'choices' */
containerOuter: string;
/** @default 'choices__inner' */
containerInner: string;
/** @default 'choices__input' */
input: string;
/** @default 'choices__input--cloned' */
inputCloned: string;
/** @default 'choices__list' */
list: string;
/** @default 'choices__list--multiple' */
listItems: string;
/** @default 'choices__list--single' */
listSingle: string;
/** @default 'choices__list--dropdown' */
listDropdown: string;
/** @default 'choices__item' */
item: string;
/** @default 'choices__item--selectable' */
itemSelectable: string;
/** @default 'choices__item--disabled' */
itemDisabled: string;
/** @default 'choices__item--choice' */
itemChoice: string;
/** @default 'choices__placeholder' */
placeholder: string;
/** @default 'choices__group' */
group: string;
/** @default 'choices__heading' */
groupHeading: string;
/** @default 'choices__button' */
button: string;
/** @default 'is-active' */
activeState: string;
/** @default 'is-focused' */
focusState: string;
/** @default 'is-open' */
openState: string;
/** @default 'is-disabled' */
disabledState: string;
/** @default 'is-highlighted' */
highlightedState: string;
/** @default 'is-selected' */
selectedState: string;
/** @default 'is-flipped' */
flippedState: string;
/** @default 'is-loading' */
loadingState: string;
/** @default 'has-no-results' */
noResults: string;
/** @default 'has-no-choices' */
noChoices: string;
}

View File

@ -0,0 +1,11 @@
export type EventType =
| 'addItem'
| 'removeItem'
| 'highlightItem'
| 'unhighlightItem'
| 'choice'
| 'change'
| 'search'
| 'showDropdown'
| 'hideDropdown'
| 'highlightChoice';

View File

@ -0,0 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export interface Group {
id?: number;
active?: boolean;
disabled?: boolean;
value: any;
}

View File

@ -0,0 +1,6 @@
import { Choice } from './choice';
export interface Item extends Choice {
choiceId?: number;
highlighted?: boolean;
}

View File

@ -0,0 +1,11 @@
export interface KeyCodeMap {
BACK_KEY: 46;
DELETE_KEY: 8;
ENTER_KEY: 13;
A_KEY: 65;
ESC_KEY: 27;
UP_KEY: 38;
DOWN_KEY: 40;
PAGE_UP_KEY: 33;
PAGE_DOWN_KEY: 34;
}

View File

@ -0,0 +1,5 @@
// @todo rename
export interface Notice {
response: boolean;
notice: string;
}

View File

@ -1,261 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { FuseOptions } from 'fuse.js';
import Choices from './choices';
export namespace Types {
export type strToEl = (
str: string,
) => HTMLElement | HTMLInputElement | HTMLOptionElement;
export type stringFunction = () => string;
export type noticeStringFunction = (value: string) => string;
export type noticeLimitFunction = (maxItemCount: number) => string;
export type filterFunction = (value: string) => boolean;
export type valueCompareFunction = (
value1: string,
value2: string,
) => boolean;
}
export interface Choice {
id?: number;
customProperties?: Record<string, any>;
disabled?: boolean;
active?: boolean;
elementId?: number;
groupId?: number;
keyCode?: number;
label: string;
placeholder?: boolean;
selected?: boolean;
value: string;
score?: number;
choices?: Choice[];
}
export interface Group {
id?: number;
active?: boolean;
disabled?: boolean;
value: any;
}
export interface Item extends Choice {
choiceId?: number;
highlighted?: boolean;
}
/**
* Events fired by Choices behave the same as standard events. Each event is triggered on the element passed to Choices (accessible via `this.passedElement`. Arguments are accessible within the `event.detail` object.
*/
export interface EventMap {
/**
* Triggered each time an item is added (programmatically or by the user).
*
* **Input types affected:** text, select-one, select-multiple
*
* Arguments: id, value, label, groupValue, keyCode
*/
addItem: CustomEvent<{
id: number;
value: string;
label: string;
groupValue: string;
keyCode: number;
}>;
/**
* Triggered each time an item is removed (programmatically or by the user).
*
* **Input types affected:** text, select-one, select-multiple
*
* Arguments: id, value, label, groupValue
*/
removeItem: CustomEvent<{
id: number;
value: string;
label: string;
groupValue: string;
}>;
/**
* Triggered each time an item is highlighted.
*
* **Input types affected:** text, select-multiple
*
* Arguments: id, value, label, groupValue
*/
highlightItem: CustomEvent<{
id: number;
value: string;
label: string;
groupValue: string;
}>;
/**
* Triggered each time an item is unhighlighted.
*
* **Input types affected:** text, select-multiple
*
* Arguments: id, value, label, groupValue
*/
unhighlightItem: CustomEvent<{
id: number;
value: string;
label: string;
groupValue: string;
}>;
/**
* Triggered each time a choice is selected **by a user**, regardless if it changes the value of the input.
*
* **Input types affected:** select-one, select-multiple
*
* Arguments: choice: Choice
*/
choice: CustomEvent<{ choice: Choice }>;
/**
* Triggered each time an item is added/removed **by a user**.
*
* **Input types affected:** text, select-one, select-multiple
*
* Arguments: value
*/
change: CustomEvent<{ value: string }>;
/**
* Triggered when a user types into an input to search choices.
*
* **Input types affected:** select-one, select-multiple
*
* Arguments: value, resultCount
*/
search: CustomEvent<{ value: string; resultCount: number }>;
/**
* Triggered when the dropdown is shown.
*
* **Input types affected:** select-one, select-multiple
*
* Arguments: -
*/
showDropdown: CustomEvent<undefined>;
/**
* Triggered when the dropdown is hidden.
*
* **Input types affected:** select-one, select-multiple
*
* Arguments: -
*/
hideDropdown: CustomEvent<undefined>;
/**
* Triggered when a choice from the dropdown is highlighted.
*
* Input types affected: select-one, select-multiple
* Arguments: el is the choice.passedElement that was affected.
*/
highlightChoice: CustomEvent<{ el: PassedElement }>;
}
export interface KeyCodeMap {
BACK_KEY: 46;
DELETE_KEY: 8;
ENTER_KEY: 13;
A_KEY: 65;
ESC_KEY: 27;
UP_KEY: 38;
DOWN_KEY: 40;
PAGE_UP_KEY: 33;
PAGE_DOWN_KEY: 34;
}
export type ActionType =
| 'ADD_CHOICE'
| 'FILTER_CHOICES'
| 'ACTIVATE_CHOICES'
| 'CLEAR_CHOICES'
| 'ADD_GROUP'
| 'ADD_ITEM'
| 'REMOVE_ITEM'
| 'HIGHLIGHT_ITEM'
| 'CLEAR_ALL'
| 'RESET_TO'
| 'SET_IS_LOADING';
/** Classes added to HTML generated by By default classnames follow the BEM notation. */
export interface ClassNames {
/** @default 'choices' */
containerOuter: string;
/** @default 'choices__inner' */
containerInner: string;
/** @default 'choices__input' */
input: string;
/** @default 'choices__input--cloned' */
inputCloned: string;
/** @default 'choices__list' */
list: string;
/** @default 'choices__list--multiple' */
listItems: string;
/** @default 'choices__list--single' */
listSingle: string;
/** @default 'choices__list--dropdown' */
listDropdown: string;
/** @default 'choices__item' */
item: string;
/** @default 'choices__item--selectable' */
itemSelectable: string;
/** @default 'choices__item--disabled' */
itemDisabled: string;
/** @default 'choices__item--choice' */
itemChoice: string;
/** @default 'choices__placeholder' */
placeholder: string;
/** @default 'choices__group' */
group: string;
/** @default 'choices__heading' */
groupHeading: string;
/** @default 'choices__button' */
button: string;
/** @default 'is-active' */
activeState: string;
/** @default 'is-focused' */
focusState: string;
/** @default 'is-open' */
openState: string;
/** @default 'is-disabled' */
disabledState: string;
/** @default 'is-highlighted' */
highlightedState: string;
/** @default 'is-selected' */
selectedState: string;
/** @default 'is-flipped' */
flippedState: string;
/** @default 'is-loading' */
loadingState: string;
/** @default 'has-no-results' */
noResults: string;
/** @default 'has-no-choices' */
noChoices: string;
}
export interface PassedElement extends HTMLElement {
classNames: ClassNames;
element: (HTMLInputElement | HTMLSelectElement) & {
// Extends HTMLElement addEventListener with Choices events
addEventListener<K extends keyof EventMap>(
type: K,
listener: (
this: HTMLInputElement | HTMLSelectElement,
ev: EventMap[K],
) => void,
options?: boolean | AddEventListenerOptions,
): void;
};
type: 'text' | 'select-one' | 'select-multiple';
isDisabled: boolean;
parentInstance: Choices;
}
import { Choices } from './choices';
import { Choice } from './choice';
import { ClassNames } from './class-names';
import { PositionOptionsType } from './position-options-type';
import { Types } from './types';
/**
* Choices options interface
@ -370,7 +118,7 @@ export interface Options {
*
* @default null
*/
addItemFilter: string | RegExp | Types.filterFunction | null;
addItemFilter: string | RegExp | Types.FilterFunction | null;
/**
* The text that is shown when a user has inputted a new item but has not pressed the enter key. To access the current input value, pass a function with a `value` argument (see the **default config** [https://github.com/jshjohnson/Choices#setup] for an example), otherwise pass a string.
@ -382,7 +130,7 @@ export interface Options {
* (value) => `Press Enter to add <b>"${value}"</b>`;
* ```
*/
addItemText: string | Types.noticeStringFunction;
addItemText: string | Types.NoticeStringFunction;
/**
* Whether a user can remove items.
@ -492,7 +240,7 @@ export interface Options {
*
* @default 'auto'
*/
position: 'auto' | 'top' | 'bottom';
position: PositionOptionsType;
/**
* Whether the scroll position should reset after adding an item.
@ -620,7 +368,7 @@ export interface Options {
*
* @default 'No results found'
*/
noResultsText: string | Types.stringFunction;
noResultsText: string | Types.StringFunction;
/**
* The text that is shown when a user has selected all possible choices. Optionally pass a function returning a string.
@ -629,7 +377,7 @@ export interface Options {
*
* @default 'No choices to choose from'
*/
noChoicesText: string | Types.stringFunction;
noChoicesText: string | Types.StringFunction;
/**
* The text that is shown when a user hovers over a selectable choice.
@ -650,14 +398,14 @@ export interface Options {
* (maxItemCount) => `Only ${maxItemCount} values can be added.`;
* ```
*/
maxItemText: string | Types.noticeLimitFunction;
maxItemText: string | Types.NoticeLimitFunction;
/**
* If no duplicates are allowed, and the value already exists in the array.
*
* @default 'Only unique values can be added'
*/
uniqueItemText: string | Types.noticeStringFunction;
uniqueItemText: string | Types.NoticeStringFunction;
/**
* The text that is shown when addItemFilter is passed and it returns false
@ -666,7 +414,7 @@ export interface Options {
*
* @default 'Only values matching specific conditions can be added'
*/
customAddItemText: string | Types.noticeStringFunction;
customAddItemText: string | Types.NoticeStringFunction;
/**
* Compare choice and value in appropriate way (e.g. deep equality for objects). To compare choice and value, pass a function with a `valueComparer` argument (see the [default config](https://github.com/jshjohnson/Choices#setup) for an example).
@ -678,7 +426,7 @@ export interface Options {
* (choice, item) => choice === item;
* ```
*/
valueComparer: Types.valueCompareFunction;
valueComparer: Types.ValueCompareFunction;
/**
* Classes added to HTML generated by By default classnames follow the BEM notation.
@ -737,18 +485,5 @@ export interface Options {
*
* @default null
*/
callbackOnCreateTemplates: ((template: Types.strToEl) => void) | null;
}
// @todo rename
export interface Notice {
response: boolean;
notice: string;
}
export interface State {
choices: Choice[];
groups: Group[];
items: Item[];
loading: boolean;
callbackOnCreateTemplates: ((template: Types.StrToEl) => void) | null;
}

View File

@ -0,0 +1 @@
export type PassedElementType = 'text' | 'select-one' | 'select-multiple';

View File

@ -0,0 +1,138 @@
import { Choices } from './choices';
import { Choice } from './choice';
import { ClassNames } from './class-names';
import { EventType } from './event-type';
import { PassedElementType } from './passed-element-type';
export interface PassedElement extends HTMLElement {
classNames: ClassNames;
element: (HTMLInputElement | HTMLSelectElement) & {
// Extends HTMLElement addEventListener with Choices events
addEventListener<K extends EventType>(
type: K,
listener: (
this: HTMLInputElement | HTMLSelectElement,
ev: EventMap[K],
) => void,
options?: boolean | AddEventListenerOptions,
): void;
};
type: PassedElementType;
isDisabled: boolean;
parentInstance: Choices;
}
/**
* Events fired by Choices behave the same as standard events. Each event is triggered on the element passed to Choices (accessible via `this.passedElement`. Arguments are accessible within the `event.detail` object.
*/
export interface EventMap {
/**
* Triggered each time an item is added (programmatically or by the user).
*
* **Input types affected:** text, select-one, select-multiple
*
* Arguments: id, value, label, groupValue, keyCode
*/
addItem: CustomEvent<{
id: number;
value: string;
label: string;
groupValue: string;
keyCode: number;
}>;
/**
* Triggered each time an item is removed (programmatically or by the user).
*
* **Input types affected:** text, select-one, select-multiple
*
* Arguments: id, value, label, groupValue
*/
removeItem: CustomEvent<{
id: number;
value: string;
label: string;
groupValue: string;
}>;
/**
* Triggered each time an item is highlighted.
*
* **Input types affected:** text, select-multiple
*
* Arguments: id, value, label, groupValue
*/
highlightItem: CustomEvent<{
id: number;
value: string;
label: string;
groupValue: string;
}>;
/**
* Triggered each time an item is unhighlighted.
*
* **Input types affected:** text, select-multiple
*
* Arguments: id, value, label, groupValue
*/
unhighlightItem: CustomEvent<{
id: number;
value: string;
label: string;
groupValue: string;
}>;
/**
* Triggered each time a choice is selected **by a user**, regardless if it changes the value of the input.
*
* **Input types affected:** select-one, select-multiple
*
* Arguments: choice: Choice
*/
choice: CustomEvent<{ choice: Choice }>;
/**
* Triggered each time an item is added/removed **by a user**.
*
* **Input types affected:** text, select-one, select-multiple
*
* Arguments: value
*/
change: CustomEvent<{ value: string }>;
/**
* Triggered when a user types into an input to search choices.
*
* **Input types affected:** select-one, select-multiple
*
* Arguments: value, resultCount
*/
search: CustomEvent<{ value: string; resultCount: number }>;
/**
* Triggered when the dropdown is shown.
*
* **Input types affected:** select-one, select-multiple
*
* Arguments: -
*/
showDropdown: CustomEvent<undefined>;
/**
* Triggered when the dropdown is hidden.
*
* **Input types affected:** select-one, select-multiple
*
* Arguments: -
*/
hideDropdown: CustomEvent<undefined>;
/**
* Triggered when a choice from the dropdown is highlighted.
*
* Input types affected: select-one, select-multiple
* Arguments: el is the choice.passedElement that was affected.
*/
highlightChoice: CustomEvent<{ el: PassedElement }>;
}

View File

@ -0,0 +1 @@
export type PositionOptionsType = 'auto' | 'top' | 'bottom';

View File

@ -0,0 +1,10 @@
import { Choice } from './choice';
import { Group } from './group';
import { Item } from './item';
export interface State {
choices: Choice[];
groups: Group[];
items: Item[];
loading: boolean;
}

View File

@ -0,0 +1,13 @@
export namespace Types {
export type StrToEl = (
str: string,
) => HTMLElement | HTMLInputElement | HTMLOptionElement;
export type StringFunction = () => string;
export type NoticeStringFunction = (value: string) => string;
export type NoticeLimitFunction = (maxItemCount: number) => string;
export type FilterFunction = (value: string) => boolean;
export type ValueCompareFunction = (
value1: string,
value2: string,
) => boolean;
}

View File

@ -84,7 +84,7 @@ describe('utils', () => {
expect(getType([])).to.equal('Array');
expect(getType(() => {})).to.equal('Function');
expect(getType(new Error())).to.equal('Error');
expect(getType(new RegExp(/''/g))).to.equal('RegExp');
expect(getType(/''/g)).to.equal('RegExp');
expect(getType(new String())).to.equal('String'); // eslint-disable-line
expect(getType('')).to.equal('String');
});

View File

@ -1,7 +1,8 @@
import { EventMap, Choice } from '../interfaces';
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Choice } from '../interfaces/choice';
import { EventType } from '../interfaces/event-type';
export const getRandomNumber = (min: number, max: number): number =>
Math.floor(Math.random() * (max - min) + min);
@ -32,11 +33,12 @@ export const wrap = (
element: HTMLElement,
wrapper: HTMLElement = document.createElement('div'),
): HTMLElement => {
if (element.parentNode) {
if (element.nextSibling) {
element.parentNode &&
element.parentNode.insertBefore(wrapper, element.nextSibling);
} else {
element.parentNode && element.parentNode.appendChild(wrapper);
element.parentNode.appendChild(wrapper);
}
}
return wrapper.appendChild(element);
@ -138,7 +140,7 @@ export const sortByScore = (
export const dispatchEvent = (
element: HTMLElement,
type: keyof EventMap,
type: EventType,
customArgs: object | null = null,
): boolean => {
const event = new CustomEvent(type, {
@ -155,7 +157,7 @@ export const existsInArray = (
value: string,
key = 'value',
): boolean =>
array.some(item => {
array.some((item) => {
if (typeof value === 'string') {
return item[key] === value.trim();
}
@ -176,5 +178,5 @@ export const diff = (
const aKeys = Object.keys(a).sort();
const bKeys = Object.keys(b).sort();
return aKeys.filter(i => bKeys.indexOf(i) < 0);
return aKeys.filter((i) => bKeys.indexOf(i) < 0);
};

View File

@ -1,6 +1,6 @@
import { expect } from 'chai';
import { Choice } from '../interfaces/choice';
import choices, { defaultState } from './choices';
import { Choice } from '../interfaces';
describe('reducers/choices', () => {
it('should return same state when no action matches', () => {
@ -178,7 +178,7 @@ describe('reducers/choices', () => {
score,
},
],
}).find(choice => choice.id === id);
}).find((choice) => choice.id === id);
expect(actualResponse).to.eql(expectedResponse);
});

View File

@ -1,4 +1,3 @@
import { Choice } from '../interfaces';
import {
AddChoiceAction,
FilterChoicesAction,
@ -6,6 +5,7 @@ import {
ClearChoicesAction,
} from '../actions/choices';
import { AddItemAction, RemoveItemAction } from '../actions/items';
import { Choice } from '../interfaces/choice';
export const defaultState = [];
@ -15,11 +15,12 @@ type ActionTypes =
| ActivateChoicesAction
| ClearChoicesAction
| AddItemAction
| RemoveItemAction;
| RemoveItemAction
| Record<string, never>;
export default function choices(
state: Choice[] = defaultState,
action: ActionTypes,
action: ActionTypes = {},
): Choice[] {
switch (action.type) {
case 'ADD_CHOICE': {
@ -52,7 +53,7 @@ export default function choices(
// When an item is added and it has an associated choice,
// we want to disable it so it can't be chosen again
if (addItemAction.choiceId > -1) {
return state.map(obj => {
return state.map((obj) => {
const choice = obj;
if (choice.id === parseInt(`${addItemAction.choiceId}`, 10)) {
choice.selected = true;
@ -71,7 +72,7 @@ export default function choices(
// When an item is removed and it has an associated choice,
// we want to re-enable it so it can be chosen again
if (removeItemAction.choiceId && removeItemAction.choiceId > -1) {
return state.map(obj => {
return state.map((obj) => {
const choice = obj;
if (choice.id === parseInt(`${removeItemAction.choiceId}`, 10)) {
choice.selected = false;
@ -87,7 +88,7 @@ export default function choices(
case 'FILTER_CHOICES': {
const filterChoicesAction = action as FilterChoicesAction;
return state.map(obj => {
return state.map((obj) => {
const choice = obj;
// Set active state based on whether choice is
// within filtered results
@ -108,7 +109,7 @@ export default function choices(
case 'ACTIVATE_CHOICES': {
const activateChoicesAction = action as ActivateChoicesAction;
return state.map(obj => {
return state.map((obj) => {
const choice = obj;
choice.active = activateChoicesAction.active;

View File

@ -1,14 +1,15 @@
import { Group, State } from '../interfaces';
import { AddGroupAction } from '../actions/groups';
import { ClearChoicesAction } from '../actions/choices';
import { Group } from '../interfaces/group';
import { State } from '../interfaces/state';
export const defaultState = [];
type ActionTypes = AddGroupAction | ClearChoicesAction;
type ActionTypes = AddGroupAction | ClearChoicesAction | Record<string, never>;
export default function groups(
state: Group[] = defaultState,
action: ActionTypes,
action: ActionTypes = {},
): State['groups'] {
switch (action.type) {
case 'ADD_GROUP': {

View File

@ -57,7 +57,7 @@ describe('reducers/items', () => {
});
it('unhighlights all highlighted items', () => {
actualResponse.forEach(item => {
actualResponse.forEach((item) => {
expect(item.highlighted).to.equal(false);
});
});

View File

@ -1,17 +1,22 @@
import { Item, State } from '../interfaces';
import {
AddItemAction,
RemoveItemAction,
HighlightItemAction,
} from '../actions/items';
import { Item } from '../interfaces/item';
import { State } from '../interfaces/state';
export const defaultState = [];
type ActionTypes = AddItemAction | RemoveItemAction | HighlightItemAction;
type ActionTypes =
| AddItemAction
| RemoveItemAction
| HighlightItemAction
| Record<string, never>;
export default function items(
state: Item[] = defaultState,
action: ActionTypes,
action: ActionTypes = {},
): State['items'] {
switch (action.type) {
case 'ADD_ITEM': {
@ -43,7 +48,7 @@ export default function items(
case 'REMOVE_ITEM': {
// Set item to inactive
return state.map(obj => {
return state.map((obj) => {
const item = obj;
if (item.id === action.id) {
item.active = false;
@ -56,7 +61,7 @@ export default function items(
case 'HIGHLIGHT_ITEM': {
const highlightItemAction = action as HighlightItemAction;
return state.map(obj => {
return state.map((obj) => {
const item = obj;
if (item.id === highlightItemAction.id) {
item.highlighted = highlightItemAction.highlighted;

View File

@ -1,13 +1,13 @@
import { SetIsLoadingAction } from '../actions/misc';
import { State } from '../interfaces';
import { State } from '../interfaces/state';
export const defaultState = false;
type ActionTypes = SetIsLoadingAction;
type ActionTypes = SetIsLoadingAction | Record<string, never>;
const general = (
state = defaultState,
action: ActionTypes,
action: ActionTypes = {},
): State['loading'] => {
switch (action.type) {
case 'SET_IS_LOADING': {

View File

@ -161,7 +161,7 @@ describe('reducers/store', () => {
describe('activeItems getter', () => {
it('returns items that are active', () => {
const expectedResponse = state.items.filter(item => item.active);
const expectedResponse = state.items.filter((item) => item.active);
expect(instance.activeItems).to.eql(expectedResponse);
});
});
@ -169,7 +169,7 @@ describe('reducers/store', () => {
describe('highlightedActiveItems getter', () => {
it('returns items that are active and highlighted', () => {
const expectedResponse = state.items.filter(
item => item.highlighted && item.active,
(item) => item.highlighted && item.active,
);
expect(instance.highlightedActiveItems).to.eql(expectedResponse);
});
@ -184,7 +184,9 @@ describe('reducers/store', () => {
describe('activeChoices getter', () => {
it('returns choices that are active', () => {
const expectedResponse = state.choices.filter(choice => choice.active);
const expectedResponse = state.choices.filter(
(choice) => choice.active,
);
expect(instance.activeChoices).to.eql(expectedResponse);
});
});
@ -192,7 +194,7 @@ describe('reducers/store', () => {
describe('selectableChoices getter', () => {
it('returns choices that are not disabled', () => {
const expectedResponse = state.choices.filter(
choice => !choice.disabled,
(choice) => !choice.disabled,
);
expect(instance.selectableChoices).to.eql(expectedResponse);
});
@ -201,7 +203,7 @@ describe('reducers/store', () => {
describe('searchableChoices getter', () => {
it('returns choices that are not placeholders and are selectable', () => {
const expectedResponse = state.choices.filter(
choice => !choice.disabled && !choice.placeholder,
(choice) => !choice.disabled && !choice.placeholder,
);
expect(instance.searchableChoices).to.eql(expectedResponse);
});
@ -212,7 +214,7 @@ describe('reducers/store', () => {
it('returns active choice by passed id', () => {
const id = '1';
const expectedResponse = state.choices.find(
choice => choice.id === parseInt(id, 10),
(choice) => choice.id === parseInt(id, 10),
);
const actualResponse = instance.getChoiceById(id);
expect(actualResponse).to.eql(expectedResponse);
@ -224,7 +226,7 @@ describe('reducers/store', () => {
it('returns placeholder choice', () => {
const expectedResponse = state.choices
.reverse()
.find(choice => choice.placeholder);
.find((choice) => choice.placeholder);
expect(instance.getPlaceholderChoice).to.eql(expectedResponse);
});
});
@ -238,7 +240,7 @@ describe('reducers/store', () => {
describe('activeGroups getter', () => {
it('returns active groups', () => {
const expectedResponse = state.groups.filter(group => group.active);
const expectedResponse = state.groups.filter((group) => group.active);
expect(instance.activeGroups).to.eql(expectedResponse);
});
});
@ -246,7 +248,7 @@ describe('reducers/store', () => {
describe('getGroupById', () => {
it('returns group by id', () => {
const id = 1;
const expectedResponse = state.groups.find(group => group.id === id);
const expectedResponse = state.groups.find((group) => group.id === id);
const actualResponse = instance.getGroupById(id);
expect(actualResponse).to.eql(expectedResponse);
});

View File

@ -1,7 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { createStore, Store as IStore, AnyAction } from 'redux';
import { Choice } from '../interfaces/choice';
import { Group } from '../interfaces/group';
import { Item } from '../interfaces/item';
import { State } from '../interfaces/state';
import rootReducer from '../reducers/index';
import { Choice, Group, Item, State } from '../interfaces';
export default class Store {
_store: IStore;
@ -46,14 +49,14 @@ export default class Store {
* Get active items from store
*/
get activeItems(): Item[] {
return this.items.filter(item => item.active === true);
return this.items.filter((item) => item.active === true);
}
/**
* Get highlighted items from store
*/
get highlightedActiveItems(): Item[] {
return this.items.filter(item => item.active && item.highlighted);
return this.items.filter((item) => item.active && item.highlighted);
}
/**
@ -67,21 +70,23 @@ export default class Store {
* Get active choices from store
*/
get activeChoices(): Choice[] {
return this.choices.filter(choice => choice.active === true);
return this.choices.filter((choice) => choice.active === true);
}
/**
* Get selectable choices from store
*/
get selectableChoices(): Choice[] {
return this.choices.filter(choice => choice.disabled !== true);
return this.choices.filter((choice) => choice.disabled !== true);
}
/**
* Get choices that can be searched (excluding placeholders)
*/
get searchableChoices(): Choice[] {
return this.selectableChoices.filter(choice => choice.placeholder !== true);
return this.selectableChoices.filter(
(choice) => choice.placeholder !== true,
);
}
/**
@ -90,7 +95,7 @@ export default class Store {
get placeholderChoice(): Choice | undefined {
return [...this.choices]
.reverse()
.find(choice => choice.placeholder === true);
.find((choice) => choice.placeholder === true);
}
/**
@ -106,10 +111,10 @@ export default class Store {
get activeGroups(): Group[] {
const { groups, choices } = this;
return groups.filter(group => {
return groups.filter((group) => {
const isActive = group.active === true && group.disabled === false;
const hasActiveOptions = choices.some(
choice => choice.active === true && choice.disabled === false,
(choice) => choice.active === true && choice.disabled === false,
);
return isActive && hasActiveOptions;
@ -127,13 +132,13 @@ export default class Store {
* Get single choice by it's ID
*/
getChoiceById(id: string): Choice | undefined {
return this.activeChoices.find(choice => choice.id === parseInt(id, 10));
return this.activeChoices.find((choice) => choice.id === parseInt(id, 10));
}
/**
* Get group by group id
*/
getGroupById(id: number): Group | undefined {
return this.groups.find(group => group.id === id);
return this.groups.find((group) => group.id === id);
}
}

View File

@ -1,10 +1,14 @@
import { ClassNames, Item, Choice, Group, PassedElement } from './interfaces';
/**
* Helpers to create HTML elements used by Choices
* Can be overridden by providing `callbackOnCreateTemplates` option
*/
import { Choice } from './interfaces/choice';
import { ClassNames } from './interfaces/class-names';
import { Group } from './interfaces/group';
import { Item } from './interfaces/item';
import { PassedElementType } from './interfaces/passed-element-type';
const templates = {
containerOuter(
{ containerOuter }: Pick<ClassNames, 'containerOuter'>,
@ -12,7 +16,7 @@ const templates = {
isSelectElement: boolean,
isSelectOneElement: boolean,
searchEnabled: boolean,
passedElementType: PassedElement['type'],
passedElementType: PassedElementType,
): HTMLDivElement {
const div = Object.assign(document.createElement('div'), {
className: containerOuter,