Merge pull request #977 from Choices-js/fix-selenium-workflow
Update dependencies, refactor to remove dependency cycles, fix GitHub Action workflows
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 24 KiB |
BIN
.github/actions-scripts/__snapshots__/edge-win32.png
vendored
Normal file
After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 104 KiB |
Before Width: | Height: | Size: 268 KiB After Width: | Height: | Size: 279 KiB |
11
.github/actions-scripts/puppeteer.js
vendored
|
@ -11,8 +11,11 @@ const server = require('../../server');
|
|||
async function test() {
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
const artifactsPath = 'screenshot';
|
||||
const snapshotName = `puppeteer-${process.platform}.png`;
|
||||
let error;
|
||||
let pixelDifference;
|
||||
let diff;
|
||||
|
||||
if (!server.listening) await once(server, 'listening');
|
||||
|
||||
|
@ -32,8 +35,6 @@ async function test() {
|
|||
await page.keyboard.press('ArrowDown');
|
||||
await page.keyboard.press('ArrowDown');
|
||||
|
||||
const snapshotName = `puppeteer-${process.platform}.png`;
|
||||
const artifactsPath = 'screenshot';
|
||||
mkdirSync(artifactsPath, { recursive: true });
|
||||
const imageBuffer = await page.screenshot({
|
||||
path: path.join(artifactsPath, snapshotName),
|
||||
|
@ -46,7 +47,7 @@ async function test() {
|
|||
readFileSync(path.resolve(__dirname, `./__snapshots__/${snapshotName}`)),
|
||||
);
|
||||
const { width, height } = screenshot;
|
||||
const diff = new PNG({ width, height });
|
||||
diff = new PNG({ width, height });
|
||||
pixelDifference = pixelmatch(
|
||||
screenshot.data,
|
||||
snapshot.data,
|
||||
|
@ -57,11 +58,13 @@ async function test() {
|
|||
threshold: 0.6,
|
||||
},
|
||||
);
|
||||
writeFileSync(path.join(artifactsPath, 'diff.png'), PNG.sync.write(diff));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
error = err;
|
||||
} finally {
|
||||
if (diff) {
|
||||
writeFileSync(path.join(artifactsPath, 'diff-' + snapshotName), PNG.sync.write(diff));
|
||||
}
|
||||
await Promise.all([
|
||||
browser.close(),
|
||||
new Promise(resolve => server.close(resolve)),
|
||||
|
|
16
.github/workflows/browsers.yml
vendored
|
@ -18,14 +18,12 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-latest, macos-latest]
|
||||
browser: [ie, firefox, safari]
|
||||
browser: [edge, firefox, safari, chrome]
|
||||
exclude:
|
||||
# On Windows, run tests with only IE and Edge
|
||||
- os: windows-latest
|
||||
browser: safari
|
||||
# On macOS, run tests with only on safari
|
||||
- os: macos-latest
|
||||
browser: ie
|
||||
browser: edge
|
||||
- os: macos-latest
|
||||
browser: chrome
|
||||
# Safari workaround is not working in Catalina
|
||||
|
@ -75,17 +73,17 @@ jobs:
|
|||
if: matrix.browser == 'safari'
|
||||
|
||||
- run: |
|
||||
brew cask install firefox
|
||||
brew install --cask firefox
|
||||
brew install geckodriver
|
||||
if: matrix.browser == 'firefox' && matrix.os == 'macos-latest'
|
||||
|
||||
- run: echo "::add-path::$env:GeckoWebDriver"
|
||||
- run: echo "$env:GeckoWebDriver" >> $GITHUB_PATH
|
||||
if: matrix.browser == 'firefox' && matrix.os == 'windows-latest'
|
||||
|
||||
- run: echo "::add-path::$env:IEWebDriver"
|
||||
if: matrix.browser == 'ie' && matrix.os == 'windows-latest'
|
||||
- run: echo "$env:EdgeWebDriver" >> $GITHUB_PATH
|
||||
if: matrix.browser == 'edge' && matrix.os == 'windows-latest'
|
||||
|
||||
- run: echo "::add-path::$env:ChromeWebDriver"
|
||||
- run: echo "$env:ChromeWebDriver" >> $GITHUB_PATH
|
||||
if: matrix.browser == 'chrome' && matrix.os == 'windows-latest'
|
||||
|
||||
- run: npm i --no-optional --no-audit selenium-webdriver pixelmatch pngjs
|
||||
|
|
25
.github/workflows/build-and-test.yml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
|||
fetch-depth: 1
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10
|
||||
node-version: 12
|
||||
- name: Build and run all tests
|
||||
run: |
|
||||
npm ci
|
||||
|
@ -32,15 +32,20 @@ jobs:
|
|||
BUNDLESIZE_GITHUB_TOKEN: ${{secrets.BUNDLESIZE_GITHUB_TOKEN}}
|
||||
FORCE_COLOR: 2
|
||||
HUSKY_SKIP_INSTALL: true
|
||||
- name: Commit built files
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git commit -m "Update build files 🏗" -a || echo "No changes to commit" && exit 0
|
||||
- name: Push changes
|
||||
uses: ad-m/github-push-action@master
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
##
|
||||
## Disabling for now. There does not appear to be a secure way to do this
|
||||
## with protected branches. See discussion:
|
||||
## https://github.community/t/how-to-push-to-protected-branches-in-a-github-action/16101
|
||||
##
|
||||
# - name: Commit built files
|
||||
# run: |
|
||||
# git config --local user.email "action@github.com"
|
||||
# git config --local user.name "GitHub Action"
|
||||
# git commit -m "Update build files 🏗" -a || echo "No changes to commit" && exit 0
|
||||
# - name: Push changes
|
||||
# uses: ad-m/github-push-action@master
|
||||
# with:
|
||||
# github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
run: bash <(curl -s https://codecov.io/bash)
|
||||
|
|
2
.github/workflows/bundlesize.yml
vendored
|
@ -18,7 +18,7 @@ jobs:
|
|||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10
|
||||
node-version: 12
|
||||
|
||||
- name: Install dependencies and build
|
||||
run: |
|
||||
|
|
4
.github/workflows/deployment.yml
vendored
|
@ -13,7 +13,7 @@ jobs:
|
|||
fetch-depth: 1
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10
|
||||
node-version: 12
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- run: npm ci
|
||||
env:
|
||||
|
@ -32,7 +32,7 @@ jobs:
|
|||
fetch-depth: 1
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10
|
||||
node-version: 12
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- name: Build
|
||||
run: |
|
||||
|
|
13
.github/workflows/lint.yml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
|||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10
|
||||
node-version: 12
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install --no-optional --no-audit --ignore-scripts
|
||||
|
@ -32,7 +32,10 @@ jobs:
|
|||
echo $CHANGED_TS
|
||||
node node_modules/eslint/bin/eslint.js $CHANGED_TS
|
||||
|
||||
- name: Lint JS bundle
|
||||
run: |
|
||||
npm run js:build
|
||||
npx eslint --no-ignore ./public/assets/scripts/*.js
|
||||
## Can't use same eslint config for TypeScript and JavaScript
|
||||
## TypeScript rules cause rule definition not found errors
|
||||
## Can be re-enabled if this is resolved: https://github.com/eslint/eslint/issues/14851
|
||||
# - name: Lint JS bundle
|
||||
# run: |
|
||||
# npm run js:build
|
||||
# npx eslint --no-ignore ./public/assets/scripts/*.js
|
||||
|
|
2
.github/workflows/unit-tests.yml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
|||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10
|
||||
node-version: 12
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install --no-optional --no-audit --ignore-scripts
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
@ -346,10 +346,10 @@ describe('Choices - select multiple', () => {
|
|||
|
||||
describe('selection limit', () => {
|
||||
/*
|
||||
{
|
||||
maxItemCount: 5,
|
||||
}
|
||||
*/
|
||||
{
|
||||
maxItemCount: 5,
|
||||
}
|
||||
*/
|
||||
const selectionLimit = 5;
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -570,7 +570,7 @@ describe('Choices - select multiple', () => {
|
|||
beforeEach(() => {
|
||||
cy.get('[data-test-hook=scrolling-dropdown]')
|
||||
.find('.choices__list--dropdown .choices__list .choices__item')
|
||||
.then($choices => {
|
||||
.then(($choices) => {
|
||||
choicesCount = $choices.length;
|
||||
});
|
||||
|
||||
|
@ -582,19 +582,20 @@ 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');
|
||||
});
|
||||
});
|
||||
|
||||
it('scrolls to next choice on down arrow', () => {
|
||||
for (let index = 0; index < choicesCount; index++) {
|
||||
for (let index = 1; index <= choicesCount; index++) {
|
||||
cy.wait(100);
|
||||
|
||||
cy.get('[data-test-hook=scrolling-dropdown]')
|
||||
.find('.choices__list--dropdown .choices__list .is-highlighted')
|
||||
.should($choice => {
|
||||
expect($choice.text().trim()).to.equal(`Choice ${index + 1}`);
|
||||
.invoke('text')
|
||||
.then((text) => {
|
||||
expect(text.trim()).to.equal(`Choice ${index}`);
|
||||
});
|
||||
|
||||
cy.get('[data-test-hook=scrolling-dropdown]')
|
||||
|
@ -617,8 +618,9 @@ describe('Choices - select multiple', () => {
|
|||
|
||||
cy.get('[data-test-hook=scrolling-dropdown]')
|
||||
.find('.choices__list--dropdown .choices__list .is-highlighted')
|
||||
.should($choice => {
|
||||
expect($choice.text().trim()).to.equal(`Choice ${index}`);
|
||||
.invoke('text')
|
||||
.then((text) => {
|
||||
expect(text.trim()).to.equal(`Choice ${index}`);
|
||||
});
|
||||
|
||||
cy.get('[data-test-hook=scrolling-dropdown]')
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
31348
package-lock.json
generated
98
package.json
|
@ -9,11 +9,11 @@
|
|||
"build": "run-p js:build css:build",
|
||||
"lint": "eslint src/scripts/**/*.ts",
|
||||
"bundlesize": "bundlesize",
|
||||
"cypress:run": "cypress run",
|
||||
"cypress:run": "cypress run --browser chrome",
|
||||
"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,58 @@
|
|||
"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",
|
||||
"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",
|
||||
"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-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",
|
||||
"eslint-webpack-plugin": "^3.1.1",
|
||||
"express": "^4.17.2",
|
||||
"husky": "^7.0.4",
|
||||
"jsdom": "^19.0.0",
|
||||
"lint-staged": "^12.1.3",
|
||||
"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": "^8.4.5",
|
||||
"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.3.0",
|
||||
"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 +127,7 @@
|
|||
"bundlesize": [
|
||||
{
|
||||
"path": "public/assets/scripts/choices.min.js",
|
||||
"maxSize": "20 kB"
|
||||
"maxSize": "25 kB"
|
||||
},
|
||||
{
|
||||
"path": "public/assets/styles/choices.min.css",
|
||||
|
|
|
@ -78,8 +78,7 @@ a:focus {
|
|||
border-radius: 2.5px;
|
||||
font-size: 14px;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
appearance: none;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
|
|
2
public/assets/styles/base.min.css
vendored
|
@ -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}
|
|
@ -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,8 +334,7 @@
|
|||
.choices__button {
|
||||
text-indent: -9999px;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
appearance: none;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
background-repeat: no-repeat;
|
||||
|
|
2
public/assets/styles/choices.min.css
vendored
|
@ -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;
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}) {
|
||||
|
|
|
@ -2,7 +2,9 @@ import { SCROLLING_SPEED } from '../constants';
|
|||
|
||||
export default class List {
|
||||
element: HTMLElement;
|
||||
|
||||
scrollPos: number;
|
||||
|
||||
height: number;
|
||||
|
||||
constructor({ element }: { element: HTMLElement }) {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
@ -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,
|
||||
};
|
12
src/scripts/interfaces/action-type.ts
Normal 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';
|
17
src/scripts/interfaces/choice.ts
Normal 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[];
|
||||
}
|
87
src/scripts/interfaces/choices.ts
Normal 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>,
|
||||
);
|
||||
}
|
55
src/scripts/interfaces/class-names.ts
Normal 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;
|
||||
}
|
11
src/scripts/interfaces/event-type.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
export type EventType =
|
||||
| 'addItem'
|
||||
| 'removeItem'
|
||||
| 'highlightItem'
|
||||
| 'unhighlightItem'
|
||||
| 'choice'
|
||||
| 'change'
|
||||
| 'search'
|
||||
| 'showDropdown'
|
||||
| 'hideDropdown'
|
||||
| 'highlightChoice';
|
8
src/scripts/interfaces/group.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export interface Group {
|
||||
id?: number;
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
value: any;
|
||||
}
|
6
src/scripts/interfaces/item.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { Choice } from './choice';
|
||||
|
||||
export interface Item extends Choice {
|
||||
choiceId?: number;
|
||||
highlighted?: boolean;
|
||||
}
|
11
src/scripts/interfaces/keycode-map.ts
Normal 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;
|
||||
}
|
5
src/scripts/interfaces/notice.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
// @todo rename
|
||||
export interface Notice {
|
||||
response: boolean;
|
||||
notice: string;
|
||||
}
|
|
@ -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;
|
||||
}
|
1
src/scripts/interfaces/passed-element-type.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export type PassedElementType = 'text' | 'select-one' | 'select-multiple';
|
138
src/scripts/interfaces/passed-element.ts
Normal 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 }>;
|
||||
}
|
1
src/scripts/interfaces/position-options-type.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export type PositionOptionsType = 'auto' | 'top' | 'bottom';
|
10
src/scripts/interfaces/state.ts
Normal 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;
|
||||
}
|
13
src/scripts/interfaces/types.ts
Normal 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;
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -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.nextSibling) {
|
||||
element.parentNode &&
|
||||
if (element.parentNode) {
|
||||
if (element.nextSibling) {
|
||||
element.parentNode.insertBefore(wrapper, element.nextSibling);
|
||||
} else {
|
||||
element.parentNode && element.parentNode.appendChild(wrapper);
|
||||
} else {
|
||||
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);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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': {
|
||||
|
|
|
@ -57,7 +57,7 @@ describe('reducers/items', () => {
|
|||
});
|
||||
|
||||
it('unhighlights all highlighted items', () => {
|
||||
actualResponse.forEach(item => {
|
||||
actualResponse.forEach((item) => {
|
||||
expect(item.highlighted).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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': {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
const ESLintPlugin = require('eslint-webpack-plugin');
|
||||
const path = require('path');
|
||||
|
||||
const include = path.resolve(__dirname, './src/scripts');
|
||||
|
@ -10,16 +11,6 @@ module.exports = {
|
|||
entry: ['./src/scripts/choices'],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
enforce: 'pre',
|
||||
loader: 'eslint-loader',
|
||||
test: /\.js?$/,
|
||||
include,
|
||||
exclude,
|
||||
options: {
|
||||
quiet: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
test: /\.ts?$/,
|
||||
|
@ -40,6 +31,12 @@ module.exports = {
|
|||
resolve: {
|
||||
extensions: [".ts", ".js"]
|
||||
},
|
||||
plugins: [new ESLintPlugin({
|
||||
context: include,
|
||||
files: '**/*.js',
|
||||
exclude: 'node_modules',
|
||||
quiet: true
|
||||
})],
|
||||
output: {
|
||||
library: 'Choices',
|
||||
libraryTarget: 'window',
|
||||
|
|