choices.js
Advanced tools
Comparing version 7.1.5 to 8.0.0
{ | ||
"name": "choices.js", | ||
"version": "7.1.5", | ||
"version": "8.0.0", | ||
"description": "A vanilla JS customisable text input/select box plugin", | ||
"main": "./public/assets/scripts/choices.min.js", | ||
"main": "./public/assets/scripts/choices.js", | ||
"types": "./types/index.d.ts", | ||
@@ -11,3 +11,2 @@ "scripts": { | ||
"lint": "eslint src/scripts", | ||
"coverage": "nyc --reporter=lcov --reporter=text --reporter=text-summary mocha", | ||
"bundlesize": "bundlesize", | ||
@@ -18,4 +17,5 @@ "cypress:run": "$(npm bin)/cypress run", | ||
"test": "run-s test:unit test:e2e", | ||
"test:unit": "mocha", | ||
"test:unit:watch": "mocha --watch --inspect=5556", | ||
"test:unit": "NODE_ENV=test mocha", | ||
"test:unit:watch": "NODE_ENV=test mocha --watch --inspect=5556", | ||
"test:unit:coverage": "NODE_ENV=test nyc --reporter=lcov --reporter=text --reporter=text-summary mocha", | ||
"test:e2e": "run-p --race start cypress:run", | ||
@@ -30,3 +30,2 @@ "js:watch": "NODE_ENV=development node server.js", | ||
"deploy": "git subtree push --prefix public origin gh-pages", | ||
"prepush": "run-s lint test:unit bundlesize", | ||
"postversion": "git push --no-verify --atomic --follow-tags", | ||
@@ -69,13 +68,16 @@ "prepublishOnly": "npm run build" | ||
"csso-cli": "^3.0.0", | ||
"cypress": "3.2.0", | ||
"eslint": "^6.5.1", | ||
"cypress": "3.5.0", | ||
"eslint": "^6.6.0", | ||
"eslint-config-airbnb-base": "^14.0.0", | ||
"eslint-config-prettier": "^6.4.0", | ||
"eslint-config-prettier": "^6.5.0", | ||
"eslint-loader": "^3.0.2", | ||
"eslint-plugin-compat": "3.3.0", | ||
"eslint-plugin-cypress": "^2.7.0", | ||
"eslint-plugin-import": "^2.18.2", | ||
"eslint-plugin-prettier": "^3.1.1", | ||
"eslint-plugin-sort-class-members": "^1.6.0", | ||
"express": "^4.16.4", | ||
"husky": "^3.0.9", | ||
"jsdom": "^15.2.0", | ||
"lint-staged": "^9.4.2", | ||
"mocha": "^6.2.2", | ||
@@ -87,3 +89,3 @@ "node-sass": "^4.12.0", | ||
"postcss-cli": "^6.1.3", | ||
"prettier": "^1.16.4", | ||
"prettier": "^1.18.2", | ||
"sinon": "^7.5.0", | ||
@@ -96,5 +98,4 @@ "webpack": "^4.41.2", | ||
"dependencies": { | ||
"classnames": "^2.2.6", | ||
"deepmerge": "^4.2.0", | ||
"fuse.js": "3.4.2", | ||
"fuse.js": "^3.4.5", | ||
"redux": "^4.0.4" | ||
@@ -101,0 +102,0 @@ }, |
450
README.md
# Choices.js [![Actions Status](https://github.com/jshjohnson/Choices/workflows/Unit%20Tests/badge.svg)](https://github.com/jshjohnson/Choices/actions) [![npm](https://img.shields.io/npm/v/choices.js.svg)](https://www.npmjs.com/package/choices.js) [![codebeat badge](https://codebeat.co/badges/55120150-5866-42d8-8010-6aaaff5d3fa1)](https://codebeat.co/projects/github-com-jshjohnson-choices-master) | ||
A vanilla, lightweight (~19kb gzipped 🎉), configurable select box/text input plugin. Similar to Select2 and Selectize but without the jQuery dependency. | ||
@@ -7,17 +8,22 @@ | ||
## TL;DR | ||
* Lightweight | ||
* No jQuery dependency | ||
* Configurable sorting | ||
* Flexible styling | ||
* Fast search/filtering | ||
* Clean API | ||
* Right-to-left support | ||
* Custom templates | ||
---- | ||
- Lightweight | ||
- No jQuery dependency | ||
- Configurable sorting | ||
- Flexible styling | ||
- Fast search/filtering | ||
- Clean API | ||
- Right-to-left support | ||
- Custom templates | ||
--- | ||
### Interested in writing your own ES6 JavaScript plugins? Check out [ES6.io](https://ES6.io/friend/JOHNSON) for great tutorials! 💪🏼 | ||
---- | ||
--- | ||
## Installation | ||
With [NPM](https://www.npmjs.com/package/choices.js): | ||
```zsh | ||
@@ -28,2 +34,3 @@ npm install choices.js | ||
With [Yarn](https://yarnpkg.com/): | ||
```zsh | ||
@@ -39,5 +46,11 @@ yarn add choices.js | ||
<!-- Include base CSS (optional) --> | ||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/choices.js/public/assets/styles/base.min.css"> | ||
<link | ||
rel="stylesheet" | ||
href="https://cdn.jsdelivr.net/npm/choices.js/public/assets/styles/base.min.css" | ||
/> | ||
<!-- Include Choices CSS --> | ||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/choices.js/public/assets/styles/choices.min.css"> | ||
<link | ||
rel="stylesheet" | ||
href="https://cdn.jsdelivr.net/npm/choices.js/public/assets/styles/choices.min.css" | ||
/> | ||
<!-- Include Choices JavaScript --> | ||
@@ -51,18 +64,16 @@ <script src="https://cdn.jsdelivr.net/npm/choices.js/public/assets/scripts/choices.min.js"></script> | ||
<!-- Include base CSS (optional) --> | ||
<link rel="stylesheet" href="public/assets/styles/base.min.css"> | ||
<link rel="stylesheet" href="public/assets/styles/base.min.css" /> | ||
<!-- Include Choices CSS --> | ||
<link rel="stylesheet" href="public/assets/styles/choices.min.css"> | ||
<link rel="stylesheet" href="public/assets/styles/choices.min.css" /> | ||
<!-- Include Choices JavaScript --> | ||
<script src="/public/assets/scripts/choices.min.js"></script> | ||
``` | ||
## Setup | ||
If you pass a selector which targets multiple elements, an array of Choices instances | ||
will be returned. If you target one element, that instance will be returned. | ||
**Note:** If you pass a selector which targets multiple elements, the first matching element will be used. Versions prior to 8.x.x would return multiple Choices instances. | ||
```js | ||
// Pass multiple elements: | ||
const [firstInstance, secondInstance] = new Choices(elements); | ||
// Pass single element: | ||
// Pass single element | ||
const element = document.querySelector('.js-choice'); | ||
const choices = new Choices(element); | ||
@@ -77,4 +88,4 @@ | ||
// Passing options (with default options) | ||
const choices = new Choices(elements, { | ||
// Passing options (with default options) | ||
const choices = new Choices(element, { | ||
silent: false, | ||
@@ -86,3 +97,3 @@ items: [], | ||
addItems: true, | ||
addItemFilterFn: null, | ||
addItemFilter: null, | ||
removeItems: true, | ||
@@ -145,3 +156,2 @@ removeItemButton: false, | ||
highlightedState: 'is-highlighted', | ||
hiddenState: 'is-hidden', | ||
flippedState: 'is-flipped', | ||
@@ -163,2 +173,3 @@ loadingState: 'is-loading', | ||
## Terminology | ||
| Word | Definition | | ||
@@ -170,7 +181,8 @@ | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
## Configuration options | ||
## Configuration options | ||
### silent | ||
**Type:** `Boolean` **Default:** `false` | ||
**Type:** `Boolean` **Default:** `false` | ||
**Input types affected:** `text`, `select-single`, `select-multiple` | ||
@@ -180,6 +192,6 @@ | ||
### items | ||
**Type:** `Array` **Default:** `[]` | ||
**Type:** `Array` **Default:** `[]` | ||
**Input types affected:** `text` | ||
@@ -212,4 +224,5 @@ | ||
### choices | ||
**Type:** `Array` **Default:** `[]` | ||
**Type:** `Array` **Default:** `[]` | ||
**Input types affected:** `select-one`, `select-multiple` | ||
@@ -241,2 +254,3 @@ | ||
### renderChoiceLimit | ||
**Type:** `Number` **Default:** `-1` | ||
@@ -249,2 +263,3 @@ | ||
### maxItemCount | ||
**Type:** `Number` **Default:** `-1` | ||
@@ -257,2 +272,3 @@ | ||
### addItems | ||
**Type:** `Boolean` **Default:** `true` | ||
@@ -265,2 +281,3 @@ | ||
### removeItems | ||
**Type:** `Boolean` **Default:** `true` | ||
@@ -273,2 +290,3 @@ | ||
### removeItemButton | ||
**Type:** `Boolean` **Default:** `false` | ||
@@ -281,2 +299,3 @@ | ||
### editItems | ||
**Type:** `Boolean` **Default:** `false` | ||
@@ -289,2 +308,3 @@ | ||
### duplicateItemsAllowed | ||
**Type:** `Boolean` **Default:** `true` | ||
@@ -297,2 +317,3 @@ | ||
### delimiter | ||
**Type:** `String` **Default:** `,` | ||
@@ -305,2 +326,3 @@ | ||
### paste | ||
**Type:** `Boolean` **Default:** `true` | ||
@@ -313,2 +335,3 @@ | ||
### searchEnabled | ||
**Type:** `Boolean` **Default:** `true` | ||
@@ -318,5 +341,6 @@ | ||
**Usage:** Whether a search area should be shown. **Note:** Multiple select boxes will *always* show search areas. | ||
**Usage:** Whether a search area should be shown. **Note:** Multiple select boxes will _always_ show search areas. | ||
### searchChoices | ||
**Type:** `Boolean` **Default:** `true` | ||
@@ -328,4 +352,4 @@ | ||
### searchFields | ||
### searchFields | ||
**Type:** `Array/String` **Default:** `['label', 'value']` | ||
@@ -338,2 +362,3 @@ | ||
### searchFloor | ||
**Type:** `Number` **Default:** `1` | ||
@@ -346,2 +371,3 @@ | ||
### searchResultLimit: 4, | ||
**Type:** `Number` **Default:** `4` | ||
@@ -354,2 +380,3 @@ | ||
### position | ||
**Type:** `String` **Default:** `auto` | ||
@@ -362,2 +389,3 @@ | ||
### resetScrollPosition | ||
**Type:** `Boolean` **Default:** `true` | ||
@@ -369,8 +397,9 @@ | ||
### addItemFilterFn | ||
**Type:** `Function` **Default:** `null` | ||
### addItemFilter | ||
**Type:** `string | RegExp | Function` **Default:** `null` | ||
**Input types affected:** `text` | ||
**Usage:** A filter function that will need to return `true` for a user to successfully add an item. | ||
**Usage:** A RegExp or string (will be passed to RegExp constructor internally) or filter function that will need to return `true` for a user to successfully add an item. | ||
@@ -382,9 +411,16 @@ **Example:** | ||
new Choices(element, { | ||
addItemFilterFn: (value) => { | ||
return (value !== 'test'); | ||
addItemFilter: (value) => { | ||
return ['orange', 'apple', 'banana'].includes(value); | ||
}; | ||
}); | ||
// only items ending to `-red` | ||
new Choices(element, { | ||
addItemFilter: '-red$'; | ||
}); | ||
``` | ||
### shouldSort | ||
**Type:** `Boolean` **Default:** `true` | ||
@@ -397,2 +433,3 @@ | ||
### shouldSortItems | ||
**Type:** `Boolean` **Default:** `false` | ||
@@ -405,2 +442,3 @@ | ||
### sortFn | ||
**Type:** `Function` **Default:** sortByAlpha | ||
@@ -424,2 +462,3 @@ | ||
### placeholder | ||
**Type:** `Boolean` **Default:** `true` | ||
@@ -429,3 +468,3 @@ | ||
**Usage:** Whether the input should show a placeholder. Used in conjunction with `placeholderValue`. If `placeholder` is set to true and no value is passed to `placeholderValue`, the passed input's placeholder attribute will be used as the placeholder value. | ||
**Usage:** Whether the input should show a placeholder. Used in conjunction with `placeholderValue`. If `placeholder` is set to true and no value is passed to `placeholderValue`, the passed input's placeholder attribute will be used as the placeholder value. | ||
@@ -446,2 +485,3 @@ **Note:** For single select boxes, the recommended way of adding a placeholder is as follows: | ||
### placeholderValue | ||
**Type:** `String` **Default:** `null` | ||
@@ -454,2 +494,3 @@ | ||
### searchPlaceholderValue | ||
**Type:** `String` **Default:** `null` | ||
@@ -462,2 +503,3 @@ | ||
### prependValue | ||
**Type:** `String` **Default:** `null` | ||
@@ -470,2 +512,3 @@ | ||
### appendValue | ||
**Type:** `String` **Default:** `null` | ||
@@ -478,2 +521,3 @@ | ||
### renderSelectedChoices | ||
**Type:** `String` **Default:** `auto` | ||
@@ -486,2 +530,3 @@ | ||
### loadingText | ||
**Type:** `String` **Default:** `Loading...` | ||
@@ -494,2 +539,3 @@ | ||
### noResultsText | ||
**Type:** `String/Function` **Default:** `No results found` | ||
@@ -502,2 +548,3 @@ | ||
### noChoicesText | ||
**Type:** `String/Function` **Default:** `No choices to choose from` | ||
@@ -510,2 +557,3 @@ | ||
### itemSelectText | ||
**Type:** `String` **Default:** `Press to select` | ||
@@ -518,2 +566,3 @@ | ||
### addItemText | ||
**Type:** `String/Function` **Default:** `Press Enter to add "${value}"` | ||
@@ -526,2 +575,3 @@ | ||
### maxItemText | ||
**Type:** `String/Function` **Default:** `Only ${maxItemCount} values can be added` | ||
@@ -534,2 +584,3 @@ | ||
### itemComparer | ||
**Type:** `Function` **Default:** `strict equality` | ||
@@ -542,2 +593,3 @@ | ||
### classNames | ||
**Type:** `Object` **Default:** | ||
@@ -567,3 +619,2 @@ | ||
highlightedState: 'is-highlighted', | ||
hiddenState: 'is-hidden', | ||
flippedState: 'is-flipped', | ||
@@ -579,5 +630,7 @@ selectedState: 'is-highlighted', | ||
## Callbacks | ||
**Note:** For each callback, `this` refers to the current instance of Choices. This can be useful if you need access to methods (`this.disable()`) or the config object (`this.config`). | ||
### callbackOnInit | ||
**Type:** `Function` **Default:** `null` | ||
@@ -590,2 +643,3 @@ | ||
### callbackOnCreateTemplates | ||
**Type:** `Function` **Default:** `null` **Arguments:** `template` | ||
@@ -596,2 +650,4 @@ | ||
**Usage:** Function to run on template creation. Through this callback it is possible to provide custom templates for the various components of Choices (see terminology). For Choices to work with custom templates, it is important you maintain the various data attributes defined [here](https://github.com/jshjohnson/Choices/blob/master/src/scripts/templates.js). | ||
If you want just extend a little original template then you may use `Choices.defaults.templates` to get access to | ||
original template function. | ||
@@ -602,7 +658,28 @@ **Example:** | ||
const example = new Choices(element, { | ||
callbackOnCreateTemplates: function (template) { | ||
callbackOnCreateTemplates: () => ({ | ||
input: (...args) => | ||
Object.assign(Choices.defaults.templates.input.call(this, ...args), { | ||
type: 'email', | ||
}), | ||
}), | ||
}); | ||
``` | ||
or more complex: | ||
```js | ||
const example = new Choices(element, { | ||
callbackOnCreateTemplates: function(template) { | ||
return { | ||
item: (classNames, data) => { | ||
return template(` | ||
<div class="${classNames.item} ${data.highlighted ? classNames.highlightedState : classNames.itemSelectable} ${data.placeholder ? classNames.placeholder : ''}" data-item data-id="${data.id}" data-value="${data.value}" ${data.active ? 'aria-selected="true"' : ''} ${data.disabled ? 'aria-disabled="true"' : ''}> | ||
<div class="${classNames.item} ${ | ||
data.highlighted | ||
? classNames.highlightedState | ||
: classNames.itemSelectable | ||
} ${ | ||
data.placeholder ? classNames.placeholder : '' | ||
}" data-item data-id="${data.id}" data-value="${data.value}" ${ | ||
data.active ? 'aria-selected="true"' : '' | ||
} ${data.disabled ? 'aria-disabled="true"' : ''}> | ||
<span>★</span> ${data.label} | ||
@@ -614,3 +691,11 @@ </div> | ||
return template(` | ||
<div class="${classNames.item} ${classNames.itemChoice} ${data.disabled ? classNames.itemDisabled : classNames.itemSelectable}" data-select-text="${this.config.itemSelectText}" data-choice ${data.disabled ? 'data-choice-disabled aria-disabled="true"' : 'data-choice-selectable'} data-id="${data.id}" data-value="${data.value}" ${data.groupId > 0 ? 'role="treeitem"' : 'role="option"'}> | ||
<div class="${classNames.item} ${classNames.itemChoice} ${ | ||
data.disabled ? classNames.itemDisabled : classNames.itemSelectable | ||
}" data-select-text="${this.config.itemSelectText}" data-choice ${ | ||
data.disabled | ||
? 'data-choice-disabled aria-disabled="true"' | ||
: 'data-choice-selectable' | ||
} data-id="${data.id}" data-value="${data.value}" ${ | ||
data.groupId > 0 ? 'role="treeitem"' : 'role="option"' | ||
}> | ||
<span>★</span> ${data.label} | ||
@@ -621,3 +706,3 @@ </div> | ||
}; | ||
} | ||
}, | ||
}); | ||
@@ -627,2 +712,3 @@ ``` | ||
## Events | ||
**Note:** 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. | ||
@@ -636,10 +722,14 @@ | ||
element.addEventListener('addItem', function(event) { | ||
// do something creative here... | ||
console.log(event.detail.id); | ||
console.log(event.detail.value); | ||
console.log(event.detail.label); | ||
console.log(event.detail.customProperties); | ||
console.log(event.detail.groupValue); | ||
}, false); | ||
element.addEventListener( | ||
'addItem', | ||
function(event) { | ||
// do something creative here... | ||
console.log(event.detail.id); | ||
console.log(event.detail.value); | ||
console.log(event.detail.label); | ||
console.log(event.detail.customProperties); | ||
console.log(event.detail.groupValue); | ||
}, | ||
false, | ||
); | ||
@@ -649,13 +739,18 @@ // or | ||
example.passedElement.element.addEventListener('addItem', function(event) { | ||
// do something creative here... | ||
console.log(event.detail.id); | ||
console.log(event.detail.value); | ||
console.log(event.detail.label); | ||
console.log(event.detail.customProperties); | ||
console.log(event.detail.groupValue); | ||
}, false); | ||
example.passedElement.element.addEventListener( | ||
'addItem', | ||
function(event) { | ||
// do something creative here... | ||
console.log(event.detail.id); | ||
console.log(event.detail.value); | ||
console.log(event.detail.label); | ||
console.log(event.detail.customProperties); | ||
console.log(event.detail.groupValue); | ||
}, | ||
false, | ||
); | ||
``` | ||
### addItem | ||
**Arguments:** `id, value, label, groupValue, keyCode` | ||
@@ -668,2 +763,3 @@ | ||
### removeItem | ||
**Arguments:** `id, value, label, groupValue` | ||
@@ -676,2 +772,3 @@ | ||
### highlightItem | ||
**Arguments:** `id, value, label, groupValue` | ||
@@ -684,2 +781,3 @@ | ||
### unhighlightItem | ||
**Arguments:** `id, value, label, groupValue` | ||
@@ -692,9 +790,12 @@ | ||
### choice | ||
**Arguments:** `value, keyCode` | ||
**Arguments:** `choice` | ||
**Input types affected:** `select-one`, `select-multiple` | ||
**Usage:** Triggered each time a choice is selected **by a user**, regardless if it changes the value of the input. | ||
`choice` is a Choice object here (see terminology or typings file) | ||
### change | ||
**Arguments:** `value` | ||
@@ -707,2 +808,3 @@ | ||
### search | ||
**Arguments:** `value`, `resultCount` | ||
@@ -715,2 +817,3 @@ | ||
### showDropdown | ||
**Arguments:** - | ||
@@ -723,2 +826,3 @@ | ||
### hideDropdown | ||
**Arguments:** - | ||
@@ -731,2 +835,3 @@ | ||
### highlightChoice | ||
**Arguments:** `el` | ||
@@ -736,5 +841,7 @@ | ||
**Usage:** Triggered when a choice from the dropdown is highlighted. The `el` argument is the HTML element node object that was affected. | ||
**Usage:** Triggered when a choice from the dropdown is highlighted. | ||
The `el` argument is choices.passedElement object that was affected. | ||
## Methods | ||
Methods can be called either directly or by chaining: | ||
@@ -745,5 +852,5 @@ | ||
const choices = new Choices(element, { | ||
addItems: false, | ||
removeItems: false, | ||
}) | ||
addItems: false, | ||
removeItems: false, | ||
}) | ||
.setValue(['Set value 1', 'Set value 2']) | ||
@@ -758,3 +865,3 @@ .disable(); | ||
choices.setValue(['Set value 1', 'Set value 2']) | ||
choices.setValue(['Set value 1', 'Set value 2']); | ||
choices.disable(); | ||
@@ -764,2 +871,3 @@ ``` | ||
### destroy(); | ||
**Input types affected:** `text`, `select-multiple`, `select-one` | ||
@@ -770,2 +878,3 @@ | ||
### init(); | ||
**Input types affected:** `text`, `select-multiple`, `select-one` | ||
@@ -778,2 +887,3 @@ | ||
### highlightAll(); | ||
**Input types affected:** `text`, `select-multiple` | ||
@@ -783,4 +893,4 @@ | ||
### unhighlightAll(); | ||
### unhighlightAll(); | ||
**Input types affected:** `text`, `select-multiple` | ||
@@ -790,4 +900,4 @@ | ||
### removeActiveItemsByValue(value); | ||
### removeActiveItemsByValue(value); | ||
**Input types affected:** `text`, `select-multiple` | ||
@@ -797,4 +907,4 @@ | ||
### removeActiveItems(excludedId); | ||
### removeActiveItems(excludedId); | ||
**Input types affected:** `text`, `select-multiple` | ||
@@ -804,4 +914,4 @@ | ||
### removeHighlightedItems(); | ||
### removeHighlightedItems(); | ||
**Input types affected:** `text`, `select-multiple` | ||
@@ -811,4 +921,4 @@ | ||
### showDropdown(); | ||
### showDropdown(); | ||
**Input types affected:** `select-one`, `select-multiple` | ||
@@ -818,4 +928,4 @@ | ||
### hideDropdown(); | ||
### hideDropdown(); | ||
**Input types affected:** `text`, `select-multiple` | ||
@@ -826,6 +936,9 @@ | ||
### setChoices(choices, value, label, replaceChoices); | ||
**Input types affected:** `select-one`, `select-multiple` | ||
**Usage:** Set choices of select input via an array of objects, a value name and a label name. This behaves the same as passing items via the `choices` option but can be called after initialising Choices. This can also be used to add groups of choices (see example 2); Optionally pass a true `replaceChoices` value to remove any existing choices. Optionally pass a `customProperties` object to add additional data to your choices (useful when searching/filtering etc). Passing an empty array as the first parameter, and a true `replaceChoices` is the same as calling `clearChoices` (see below). | ||
**Usage:** Set choices of select input via an array of objects (or function that returns array of object or promise of it), a value field name and a label field name. | ||
This behaves the similar as passing items via the `choices` option but can be called after initialising Choices. This can also be used to add groups of choices (see example 3); Optionally pass a true `replaceChoices` value to remove any existing choices. Optionally pass a `customProperties` object to add additional data to your choices (useful when searching/filtering etc). Passing an empty array as the first parameter, and a true `replaceChoices` is the same as calling `clearChoices` (see below). | ||
**Example 1:** | ||
@@ -836,7 +949,12 @@ | ||
example.setChoices([ | ||
{value: 'One', label: 'Label One', disabled: true}, | ||
{value: 'Two', label: 'Label Two', selected: true}, | ||
{value: 'Three', label: 'Label Three'}, | ||
], 'value', 'label', false); | ||
example.setChoices( | ||
[ | ||
{ value: 'One', label: 'Label One', disabled: true }, | ||
{ value: 'Two', label: 'Label Two', selected: true }, | ||
{ value: 'Three', label: 'Label Three' }, | ||
], | ||
'value', | ||
'label', | ||
false, | ||
); | ||
``` | ||
@@ -849,28 +967,56 @@ | ||
example.setChoices([{ | ||
label: 'Group one', | ||
id: 1, | ||
disabled: false, | ||
choices: [ | ||
{value: 'Child One', label: 'Child One', selected: true}, | ||
{value: 'Child Two', label: 'Child Two', disabled: true}, | ||
{value: 'Child Three', label: 'Child Three'}, | ||
] | ||
}, | ||
{ | ||
label: 'Group two', | ||
id: 2, | ||
disabled: false, | ||
choices: [ | ||
{value: 'Child Four', label: 'Child Four', disabled: true}, | ||
{value: 'Child Five', label: 'Child Five'}, | ||
{value: 'Child Six', label: 'Child Six', customProperties: { | ||
description: 'Custom description about child six', | ||
random: 'Another random custom property' | ||
}}, | ||
] | ||
}], 'value', 'label', false); | ||
// Passing a function that returns Promise of choices | ||
example.setChoices(async () => { | ||
try { | ||
const items = await fetch('/items'); | ||
return items.json(); | ||
} catch (err) { | ||
console.error(err); | ||
} | ||
}); | ||
``` | ||
**Example 3:** | ||
```js | ||
const example = new Choices(element); | ||
example.setChoices( | ||
[ | ||
{ | ||
label: 'Group one', | ||
id: 1, | ||
disabled: false, | ||
choices: [ | ||
{ value: 'Child One', label: 'Child One', selected: true }, | ||
{ value: 'Child Two', label: 'Child Two', disabled: true }, | ||
{ value: 'Child Three', label: 'Child Three' }, | ||
], | ||
}, | ||
{ | ||
label: 'Group two', | ||
id: 2, | ||
disabled: false, | ||
choices: [ | ||
{ value: 'Child Four', label: 'Child Four', disabled: true }, | ||
{ value: 'Child Five', label: 'Child Five' }, | ||
{ | ||
value: 'Child Six', | ||
label: 'Child Six', | ||
customProperties: { | ||
description: 'Custom description about child six', | ||
random: 'Another random custom property', | ||
}, | ||
}, | ||
], | ||
}, | ||
], | ||
'value', | ||
'label', | ||
false, | ||
); | ||
``` | ||
### clearChoices(); | ||
**Input types affected:** `select-one`, `select-multiple` | ||
@@ -881,2 +1027,3 @@ | ||
### getValue(valueOnly) | ||
**Input types affected:** `text`, `select-one`, `select-multiple` | ||
@@ -894,3 +1041,4 @@ | ||
### setValue(args); | ||
### setValue(items); | ||
**Input types affected:** `text` | ||
@@ -907,12 +1055,13 @@ | ||
example.setValue([ | ||
{value: 'One', label: 'Label One'}, | ||
{value: 'Two', label: 'Label Two'}, | ||
{value: 'Three', label: 'Label Three'}, | ||
{ value: 'One', label: 'Label One' }, | ||
{ value: 'Two', label: 'Label Two' }, | ||
{ value: 'Three', label: 'Label Three' }, | ||
]); | ||
// or via an array of strings | ||
example.setValue(['Four','Five','Six']); | ||
example.setValue(['Four', 'Five', 'Six']); | ||
``` | ||
### setChoiceByValue(value); | ||
**Input types affected:** `select-one`, `select-multiple` | ||
@@ -927,5 +1076,5 @@ | ||
choices: [ | ||
{value: 'One', label: 'Label One'}, | ||
{value: 'Two', label: 'Label Two', disabled: true}, | ||
{value: 'Three', label: 'Label Three'}, | ||
{ value: 'One', label: 'Label One' }, | ||
{ value: 'Two', label: 'Label Two', disabled: true }, | ||
{ value: 'Three', label: 'Label Three' }, | ||
], | ||
@@ -938,2 +1087,3 @@ }); | ||
### clearStore(); | ||
**Input types affected:** `text`, `select-one`, `select-multiple` | ||
@@ -943,4 +1093,4 @@ | ||
### clearInput(); | ||
### clearInput(); | ||
**Input types affected:** `text` | ||
@@ -950,4 +1100,4 @@ | ||
### disable(); | ||
### disable(); | ||
**Input types affected:** `text`, `select-one`, `select-multiple` | ||
@@ -958,2 +1108,3 @@ | ||
### enable(); | ||
**Input types affected:** `text`, `select-one`, `select-multiple` | ||
@@ -963,52 +1114,13 @@ | ||
## Browser compatibility | ||
### ajax(fn); | ||
**Input types affected:** `select-one`, `select-multiple` | ||
Choices is compiled using [Babel](https://babeljs.io/) targeting browsers [with more that 1% of global usage](https://github.com/jshjohnson/Choices/blob/master/.browserslistrc) and expecting that features [listed below](https://github.com/jshjohnson/Choices/blob/master/.eslintrc.json#L62) are available or polyfilled in browser. | ||
You may see exact list of target browsers by running `npx browserslist` withing this repository folder. | ||
If you need to support a browser that does not have one of the features listed below, | ||
I suggest including a polyfill from the very good [polyfill.io](https://polyfill.io/v3/): | ||
**Usage:** Populate choices/groups via a callback. | ||
**Example:** | ||
```js | ||
var example = new Choices(element); | ||
example.ajax(function(callback) { | ||
fetch(url) | ||
.then(function(response) { | ||
response.json().then(function(data) { | ||
callback(data, 'value', 'label'); | ||
}); | ||
}) | ||
.catch(function(error) { | ||
console.log(error); | ||
}); | ||
}); | ||
``` | ||
**Example 2:** | ||
If your structure differs from `data.value` and `data.key` structure you can write your own `key` and `value` into the `callback` function. This could be useful when you don't want to transform the given response. | ||
```js | ||
const example = new Choices(element) | ||
example.ajax(function(callback) { | ||
fetch(url) | ||
.then(function(response) { | ||
response.json().then(function(data) { | ||
callback(data, 'data.key', 'data.value'); | ||
}); | ||
}) | ||
.catch(function(error) { | ||
console.log(error); | ||
}); | ||
}); | ||
``` | ||
## Browser compatibility | ||
Choices is compiled using [Babel](https://babeljs.io/) to enable support for [ES5 browsers](http://caniuse.com/#feat=es5). If you need to support a browser that does not support one of the features listed below, I suggest including a polyfill from the very good [polyfill.io](https://cdn.polyfill.io/v2/docs/): | ||
**Polyfill example used for the demo:** | ||
```html | ||
<script src="https://cdn.polyfill.io/v2/polyfill.js?features=es5,fetch,Element.prototype.classList,requestAnimationFrame,Node.insertBefore,Node.firstChild,CustomEvent"></script> | ||
<script src="https://cdn.polyfill.io/v3/polyfill.min.js?features=es5,es6,fetch,Array.prototype.includes,CustomEvent,Element.prototype.closest"></script> | ||
``` | ||
@@ -1018,21 +1130,22 @@ | ||
* Array.prototype.forEach | ||
* Array.prototype.map | ||
* Array.prototype.find | ||
* Array.prototype.some | ||
* Array.prototype.includes | ||
* Array.from | ||
* Array.prototype.reduce | ||
* Array.prototype.indexOf | ||
* Object.assign | ||
* Element.prototype.classList | ||
* window.requestAnimationFrame | ||
* CustomEvent | ||
```polyfills | ||
Array.from | ||
Array.prototype.find | ||
Array.prototype.includes | ||
Symbol | ||
Symbol.iterator | ||
Object.assign | ||
CustomEvent | ||
Element.prototype.classList | ||
Element.prototype.closest | ||
``` | ||
## Development | ||
To setup a local environment: clone this repo, navigate into it's directory in a terminal window and run the following command: | ||
```npm install``` | ||
`npm install` | ||
### NPM tasks | ||
| Task | Usage | | ||
@@ -1052,8 +1165,11 @@ | ------------------------- | ------------------------------------------------------------ | | ||
## License | ||
MIT License | ||
## Web component | ||
Want to use Choices as a web component? You're in luck. Adidas have built one for their design system which can be found [here](https://github.com/adidas/choicesjs-stencil). | ||
## Misc | ||
Thanks to [@mikefrancis](https://github.com/mikefrancis/) for [sending me on a hunt](https://twitter.com/_mikefrancis/status/701797835826667520) for a non-jQuery solution for select boxes that eventually led to this being built! |
@@ -1,3 +0,1 @@ | ||
/* eslint-disable import/prefer-default-export */ | ||
export const setIsLoading = isLoading => ({ | ||
@@ -4,0 +2,0 @@ type: 'SET_IS_LOADING', |
import { ACTION_TYPES } from '../constants'; | ||
/* eslint-disable import/prefer-default-export */ | ||
export const addGroup = (value, id, active, disabled) => ({ | ||
@@ -5,0 +4,0 @@ type: ACTION_TYPES.ADD_GROUP, |
import Fuse from 'fuse.js'; | ||
import merge from 'deepmerge'; | ||
import './lib/delegate-events'; | ||
import Store from './store/store'; | ||
@@ -35,3 +34,2 @@ import { | ||
findAncestorByAttrName, | ||
fetchFromObject, | ||
isIE11, | ||
@@ -43,2 +41,4 @@ existsInArray, | ||
const USER_DEFAULTS = /** @type {Partial<import('../../types/index').Choices.Options>} */ ({}); | ||
/** | ||
@@ -48,16 +48,25 @@ * Choices | ||
*/ | ||
/** | ||
* @typedef {import('../../types/index').Choices.Choice} Choice | ||
*/ | ||
class Choices { | ||
static get defaults() { | ||
return Object.preventExtensions({ | ||
get options() { | ||
return USER_DEFAULTS; | ||
}, | ||
get templates() { | ||
return TEMPLATES; | ||
}, | ||
}); | ||
} | ||
/** | ||
* @param {string | HTMLInputElement | HTMLSelectElement} element | ||
* @param {Partial<import('../../types/index').Choices.Options>} userConfig | ||
*/ | ||
constructor(element = '[data-choice]', userConfig = {}) { | ||
if (isType('String', element)) { | ||
const elements = Array.from(document.querySelectorAll(element)); | ||
// If there are multiple elements, create a new instance | ||
// for each element besides the first one (as that already has an instance) | ||
if (elements.length > 1) { | ||
return this._generateInstances(elements, userConfig); | ||
} | ||
} | ||
this.config = merge.all( | ||
[DEFAULT_CONFIG, Choices.userDefaults, userConfig], | ||
[DEFAULT_CONFIG, Choices.defaults.options, userConfig], | ||
// When merging array configs, replace with a copy of the userConfig array, | ||
@@ -68,2 +77,15 @@ // instead of concatenating with the default array | ||
// Convert addItemFilter to function | ||
if ( | ||
userConfig.addItemFilter && | ||
typeof userConfig.addItemFilter !== 'function' | ||
) { | ||
const re = | ||
userConfig.addItemFilter instanceof RegExp | ||
? userConfig.addItemFilter | ||
: new RegExp(userConfig.addItemFilter); | ||
this.config.addItemFilter = re.test.bind(re); | ||
} | ||
const invalidConfigOptions = diff(this.config, DEFAULT_CONFIG); | ||
@@ -81,15 +103,14 @@ if (invalidConfigOptions.length) { | ||
// Retrieve triggering element (i.e. element with 'data-choice' trigger) | ||
const passedElement = isType('String', element) | ||
? document.querySelector(element) | ||
: element; | ||
const passedElement = | ||
typeof element === 'string' ? document.querySelector(element) : element; | ||
if (!passedElement) { | ||
if (!this.config.silent) { | ||
console.error( | ||
'Could not find passed element or passed element was of an invalid type', | ||
); | ||
} | ||
return; | ||
if ( | ||
!( | ||
passedElement instanceof HTMLInputElement || | ||
passedElement instanceof HTMLSelectElement | ||
) | ||
) { | ||
throw TypeError( | ||
'Expected one of the following types text|select-one|select-multiple', | ||
); | ||
} | ||
@@ -109,17 +130,13 @@ | ||
}); | ||
} else if (this._isSelectElement) { | ||
} else { | ||
this.passedElement = new WrappedSelect({ | ||
element: passedElement, | ||
classNames: this.config.classNames, | ||
template: data => this.config.templates.option(data), | ||
template: data => this._templates.option(data), | ||
}); | ||
} | ||
if (!this.passedElement) { | ||
return console.error('Passed element was of an invalid type'); | ||
} | ||
this.initialised = false; | ||
this._store = new Store(this.render); | ||
this._store = new Store(); | ||
this._initialState = {}; | ||
@@ -135,3 +152,19 @@ this._currentState = {}; | ||
this._baseId = generateId(this.passedElement.element, 'choices-'); | ||
this._direction = this.passedElement.element.getAttribute('dir') || 'ltr'; | ||
/** | ||
* setting direction in cases where it's explicitly set on passedElement | ||
* or when calculated direction is different from the document | ||
* @type {HTMLElement['dir']} | ||
*/ | ||
this._direction = this.passedElement.element.dir; | ||
if (!this._direction) { | ||
const { direction: elementDirection } = window.getComputedStyle( | ||
this.passedElement.element, | ||
); | ||
const { direction: documentDirection } = window.getComputedStyle( | ||
document.documentElement, | ||
); | ||
if (elementDirection !== documentDirection) { | ||
this._direction = elementDirection; | ||
} | ||
} | ||
this._idNames = { | ||
@@ -168,4 +201,4 @@ itemChoice: 'item-choice', | ||
if (!this.config.silent) { | ||
if (this.config.shouldSortItems === true && this._isSelectOneElement) { | ||
if (this.config.shouldSortItems === true && this._isSelectOneElement) { | ||
if (!this.config.silent) { | ||
console.warn( | ||
@@ -175,5 +208,7 @@ "shouldSortElements: Type of passed element is 'select-one', falling back to false.", | ||
} | ||
} | ||
// If element has already been initialised with Choices, fail silently | ||
if (this.passedElement.element.getAttribute('data-choice') === 'active') { | ||
// If element has already been initialised with Choices, fail silently | ||
if (this.passedElement.element.getAttribute('data-choice') === 'active') { | ||
if (!this.config.silent) { | ||
console.warn( | ||
@@ -183,2 +218,6 @@ 'Trying to initialise Choices on element already initialised', | ||
} | ||
this.initialised = true; | ||
return; | ||
} | ||
@@ -190,6 +229,2 @@ | ||
/* ======================================== | ||
= Public functions = | ||
======================================== */ | ||
init() { | ||
@@ -223,3 +258,3 @@ if (this.initialised) { | ||
// Run callback if it is a function | ||
if (callbackOnInit && isType('Function', callbackOnInit)) { | ||
if (callbackOnInit && typeof callbackOnInit === 'function') { | ||
callbackOnInit.call(this); | ||
@@ -243,4 +278,3 @@ } | ||
this.clearStore(); | ||
this.config.templates = null; | ||
this._templates = null; | ||
this.initialised = false; | ||
@@ -320,2 +354,3 @@ } | ||
this._store.items.forEach(item => this.highlightItem(item)); | ||
return this; | ||
@@ -326,2 +361,3 @@ } | ||
this._store.items.forEach(item => this.unhighlightItem(item)); | ||
return this; | ||
@@ -402,2 +438,3 @@ } | ||
selectedItems.push(itemValue); | ||
return selectedItems; | ||
@@ -409,3 +446,6 @@ }, []); | ||
setValue(args) { | ||
/** | ||
* @param {string[] | import('../../types/index').Choices.Item[]} items | ||
*/ | ||
setValue(items) { | ||
if (!this.initialised) { | ||
@@ -415,3 +455,4 @@ return this; | ||
[...args].forEach(value => this._setChoiceOrItem(value)); | ||
items.forEach(value => this._setChoiceOrItem(value)); | ||
return this; | ||
@@ -426,3 +467,3 @@ } | ||
// If only one value has been passed, convert to array | ||
const choiceValue = isType('Array', value) ? value : [value]; | ||
const choiceValue = Array.isArray(value) ? value : [value]; | ||
@@ -435,7 +476,93 @@ // Loop through each value and | ||
setChoices(choices = [], value = '', label = '', replaceChoices = false) { | ||
if (!this._isSelectElement || !value) { | ||
return this; | ||
/** | ||
* Set choices of select input via an array of objects (or function that returns array of object or promise of it), | ||
* a value field name and a label field name. | ||
* This behaves the same as passing items via the choices option but can be called after initialising Choices. | ||
* This can also be used to add groups of choices (see example 2); Optionally pass a true `replaceChoices` value to remove any existing choices. | ||
* Optionally pass a `customProperties` object to add additional data to your choices (useful when searching/filtering etc). | ||
* | ||
* **Input types affected:** select-one, select-multiple | ||
* | ||
* @template {object[] | ((instance: Choices) => object[] | Promise<object[]>)} T | ||
* @param {T} [choicesArrayOrFetcher] | ||
* @param {string} [value = 'value'] - name of `value` field | ||
* @param {string} [label = 'label'] - name of 'label' field | ||
* @param {boolean} [replaceChoices = false] - whether to replace of add choices | ||
* @returns {this | Promise<this>} | ||
* | ||
* @example | ||
* ```js | ||
* const example = new Choices(element); | ||
* | ||
* example.setChoices([ | ||
* {value: 'One', label: 'Label One', disabled: true}, | ||
* {value: 'Two', label: 'Label Two', selected: true}, | ||
* {value: 'Three', label: 'Label Three'}, | ||
* ], 'value', 'label', false); | ||
* ``` | ||
* | ||
* @example | ||
* ```js | ||
* const example = new Choices(element); | ||
* | ||
* example.setChoices(async () => { | ||
* try { | ||
* const items = await fetch('/items'); | ||
* return items.json() | ||
* } catch(err) { | ||
* console.error(err) | ||
* } | ||
* }); | ||
* ``` | ||
* | ||
* @example | ||
* ```js | ||
* const example = new Choices(element); | ||
* | ||
* example.setChoices([{ | ||
* label: 'Group one', | ||
* id: 1, | ||
* disabled: false, | ||
* choices: [ | ||
* {value: 'Child One', label: 'Child One', selected: true}, | ||
* {value: 'Child Two', label: 'Child Two', disabled: true}, | ||
* {value: 'Child Three', label: 'Child Three'}, | ||
* ] | ||
* }, | ||
* { | ||
* label: 'Group two', | ||
* id: 2, | ||
* disabled: false, | ||
* choices: [ | ||
* {value: 'Child Four', label: 'Child Four', disabled: true}, | ||
* {value: 'Child Five', label: 'Child Five'}, | ||
* {value: 'Child Six', label: 'Child Six', customProperties: { | ||
* description: 'Custom description about child six', | ||
* random: 'Another random custom property' | ||
* }}, | ||
* ] | ||
* }], 'value', 'label', false); | ||
* ``` | ||
*/ | ||
setChoices( | ||
choicesArrayOrFetcher = [], | ||
value = 'value', | ||
label = 'label', | ||
replaceChoices = false, | ||
) { | ||
if (!this.initialised) { | ||
throw new ReferenceError( | ||
`setChoices was called on a non-initialized instance of Choices`, | ||
); | ||
} | ||
if (!this._isSelectElement) { | ||
throw new TypeError(`setChoices can't be used with INPUT based Choices`); | ||
} | ||
if (typeof value !== 'string' || !value) { | ||
throw new TypeError( | ||
`value parameter must be a name of 'value' field in passed objects`, | ||
); | ||
} | ||
// Clear choices if needed | ||
@@ -446,3 +573,37 @@ if (replaceChoices) { | ||
if (!Array.isArray(choicesArrayOrFetcher)) { | ||
if (typeof choicesArrayOrFetcher !== 'function') { | ||
throw new TypeError( | ||
`.setChoices must be called either with array of choices with a function resulting into Promise of array of choices`, | ||
); | ||
} | ||
// it's a choices fetcher | ||
requestAnimationFrame(() => this._handleLoadingState(true)); | ||
const fetcher = choicesArrayOrFetcher(this); | ||
if (typeof fetcher === 'object' && typeof fetcher.then === 'function') { | ||
// that's a promise | ||
return fetcher | ||
.then(data => this.setChoices(data, value, label, replaceChoices)) | ||
.catch(err => { | ||
if (!this.config.silent) { | ||
console.error(err); | ||
} | ||
}) | ||
.then(() => this._handleLoadingState(false)) | ||
.then(() => this); | ||
} | ||
// function returned something else than promise, let's check if it's an array of choices | ||
if (!Array.isArray(fetcher)) { | ||
throw new TypeError( | ||
`.setChoices first argument function must return either array of choices or Promise, got: ${typeof fetcher}`, | ||
); | ||
} | ||
// recursion with results, it's sync and choices were cleared already | ||
return this.setChoices(fetcher, value, label, false); | ||
} | ||
this.containerOuter.removeLoadingState(); | ||
const addGroupsAndChoices = groupOrChoice => { | ||
@@ -469,3 +630,3 @@ if (groupOrChoice.choices) { | ||
this._setLoading(true); | ||
choices.forEach(addGroupsAndChoices); | ||
choicesArrayOrFetcher.forEach(addGroupsAndChoices); | ||
this._setLoading(false); | ||
@@ -478,2 +639,4 @@ | ||
this._store.dispatch(clearChoices()); | ||
return this; | ||
} | ||
@@ -483,2 +646,3 @@ | ||
this._store.dispatch(clearAll()); | ||
return this; | ||
@@ -499,19 +663,2 @@ } | ||
ajax(fn) { | ||
if (!this.initialised || !this._isSelectElement || !fn) { | ||
return this; | ||
} | ||
requestAnimationFrame(() => this._handleLoadingState(true)); | ||
fn(this._ajaxCallback()); | ||
return this; | ||
} | ||
/* ===== End of Public functions ====== */ | ||
/* ============================================= | ||
= Private functions = | ||
============================================= */ | ||
_render() { | ||
@@ -605,11 +752,13 @@ if (this._store.isLoading()) { | ||
if (this._isSearching) { | ||
notice = isType('Function', this.config.noResultsText) | ||
? this.config.noResultsText() | ||
: this.config.noResultsText; | ||
notice = | ||
typeof this.config.noResultsText === 'function' | ||
? this.config.noResultsText() | ||
: this.config.noResultsText; | ||
dropdownItem = this._getTemplate('notice', notice, 'no-results'); | ||
} else { | ||
notice = isType('Function', this.config.noChoicesText) | ||
? this.config.noChoicesText() | ||
: this.config.noChoicesText; | ||
notice = | ||
typeof this.config.noChoicesText === 'function' | ||
? this.config.noChoicesText() | ||
: this.config.noChoicesText; | ||
@@ -637,4 +786,7 @@ dropdownItem = this._getTemplate('notice', notice, 'no-choices'); | ||
_createGroupsFragment(groups, choices, fragment) { | ||
const groupFragment = fragment || document.createDocumentFragment(); | ||
_createGroupsFragment( | ||
groups, | ||
choices, | ||
fragment = document.createDocumentFragment(), | ||
) { | ||
const getGroupChoices = group => | ||
@@ -645,2 +797,3 @@ choices.filter(choice => { | ||
} | ||
return ( | ||
@@ -661,13 +814,16 @@ choice.groupId === group.id && | ||
const dropdownGroup = this._getTemplate('choiceGroup', group); | ||
groupFragment.appendChild(dropdownGroup); | ||
this._createChoicesFragment(groupChoices, groupFragment, true); | ||
fragment.appendChild(dropdownGroup); | ||
this._createChoicesFragment(groupChoices, fragment, true); | ||
} | ||
}); | ||
return groupFragment; | ||
return fragment; | ||
} | ||
_createChoicesFragment(choices, fragment, withinGroup = false) { | ||
_createChoicesFragment( | ||
choices, | ||
fragment = document.createDocumentFragment(), | ||
withinGroup = false, | ||
) { | ||
// Create a fragment to store our list items (so we don't have to update the DOM for each item) | ||
const choicesFragment = fragment || document.createDocumentFragment(); | ||
const { | ||
@@ -690,3 +846,3 @@ renderSelectedChoices, | ||
); | ||
choicesFragment.appendChild(dropdownItem); | ||
fragment.appendChild(dropdownItem); | ||
} | ||
@@ -709,2 +865,3 @@ }; | ||
} | ||
return acc; | ||
@@ -738,9 +895,8 @@ }, | ||
return choicesFragment; | ||
return fragment; | ||
} | ||
_createItemsFragment(items, fragment = null) { | ||
_createItemsFragment(items, fragment = document.createDocumentFragment()) { | ||
// Create fragment to add elements to | ||
const { shouldSortItems, sortFn, removeItemButton } = this.config; | ||
const itemListFragment = fragment || document.createDocumentFragment(); | ||
@@ -764,3 +920,3 @@ // If sorting is enabled, filter items | ||
// Append it to list | ||
itemListFragment.appendChild(listItem); | ||
fragment.appendChild(listItem); | ||
}; | ||
@@ -771,3 +927,3 @@ | ||
return itemListFragment; | ||
return fragment; | ||
} | ||
@@ -859,4 +1015,7 @@ | ||
// If we are clicking on an option | ||
const id = element.getAttribute('data-id'); | ||
const { id } = element.dataset; | ||
const choice = this._store.getChoiceById(id); | ||
if (!choice) { | ||
return; | ||
} | ||
const passedKeyCode = | ||
@@ -873,3 +1032,3 @@ activeItems[0] && activeItems[0].keyCode ? activeItems[0].keyCode : null; | ||
if (choice && !choice.selected && !choice.disabled) { | ||
if (!choice.selected && !choice.disabled) { | ||
const canAddItem = this._canAddItem(activeItems, choice.value); | ||
@@ -894,3 +1053,3 @@ | ||
// We wont to close the dropdown if we are dealing with a single select box | ||
// We want to close the dropdown if we are dealing with a single select box | ||
if (hasActiveDropdown && this._isSelectOneElement) { | ||
@@ -990,5 +1149,6 @@ this.hideDropdown(true); | ||
let canAddItem = true; | ||
let notice = isType('Function', this.config.addItemText) | ||
? this.config.addItemText(value) | ||
: this.config.addItemText; | ||
let notice = | ||
typeof this.config.addItemText === 'function' | ||
? this.config.addItemText(value) | ||
: this.config.addItemText; | ||
@@ -1005,5 +1165,6 @@ if (!this._isSelectOneElement) { | ||
canAddItem = false; | ||
notice = isType('Function', this.config.maxItemText) | ||
? this.config.maxItemText(this.config.maxItemCount) | ||
: this.config.maxItemText; | ||
notice = | ||
typeof this.config.maxItemText === 'function' | ||
? this.config.maxItemText(this.config.maxItemCount) | ||
: this.config.maxItemText; | ||
} | ||
@@ -1017,5 +1178,6 @@ | ||
canAddItem = false; | ||
notice = isType('Function', this.config.uniqueItemText) | ||
? this.config.uniqueItemText(value) | ||
: this.config.uniqueItemText; | ||
notice = | ||
typeof this.config.uniqueItemText === 'function' | ||
? this.config.uniqueItemText(value) | ||
: this.config.uniqueItemText; | ||
} | ||
@@ -1027,9 +1189,10 @@ | ||
canAddItem && | ||
isType('Function', this.config.addItemFilterFn) && | ||
!this.config.addItemFilterFn(value) | ||
typeof this.config.addItemFilter === 'function' && | ||
!this.config.addItemFilter(value) | ||
) { | ||
canAddItem = false; | ||
notice = isType('Function', this.config.customAddItemText) | ||
? this.config.customAddItemText(value) | ||
: this.config.customAddItemText; | ||
notice = | ||
typeof this.config.customAddItemText === 'function' | ||
? this.config.customAddItemText(value) | ||
: this.config.customAddItemText; | ||
} | ||
@@ -1044,56 +1207,8 @@ } | ||
_ajaxCallback() { | ||
return (results, value, label) => { | ||
if (!results || !value) { | ||
return; | ||
} | ||
const parsedResults = isType('Object', results) ? [results] : results; | ||
if ( | ||
parsedResults && | ||
isType('Array', parsedResults) && | ||
parsedResults.length | ||
) { | ||
// Remove loading states/text | ||
this._handleLoadingState(false); | ||
this._setLoading(true); | ||
// Add each result as a choice | ||
parsedResults.forEach(result => { | ||
if (result.choices) { | ||
this._addGroup({ | ||
group: result, | ||
id: result.id || null, | ||
valueKey: value, | ||
labelKey: label, | ||
}); | ||
} else { | ||
this._addChoice({ | ||
value: fetchFromObject(result, value), | ||
label: fetchFromObject(result, label), | ||
isSelected: result.selected, | ||
isDisabled: result.disabled, | ||
customProperties: result.customProperties, | ||
placeholder: result.placeholder, | ||
}); | ||
} | ||
}); | ||
this._setLoading(false); | ||
if (this._isSelectOneElement) { | ||
this._selectPlaceholderChoice(); | ||
} | ||
} else { | ||
// No results, remove loading state | ||
this._handleLoadingState(false); | ||
} | ||
}; | ||
} | ||
_searchChoices(value) { | ||
const newValue = isType('String', value) ? value.trim() : value; | ||
const currentValue = isType('String', this._currentValue) | ||
? this._currentValue.trim() | ||
: this._currentValue; | ||
const newValue = typeof value === 'string' ? value.trim() : value; | ||
const currentValue = | ||
typeof this._currentValue === 'string' | ||
? this._currentValue.trim() | ||
: this._currentValue; | ||
@@ -1121,20 +1236,42 @@ if (newValue.length < 1 && newValue === `${currentValue} `) { | ||
_addEventListeners() { | ||
window.delegateEvent.add('keyup', this._onKeyUp); | ||
window.delegateEvent.add('keydown', this._onKeyDown); | ||
window.delegateEvent.add('click', this._onClick); | ||
window.delegateEvent.add('touchmove', this._onTouchMove); | ||
window.delegateEvent.add('touchend', this._onTouchEnd); | ||
window.delegateEvent.add('mousedown', this._onMouseDown); | ||
window.delegateEvent.add('mouseover', this._onMouseOver); | ||
const { documentElement } = document; | ||
// capture events - can cancel event processing or propagation | ||
documentElement.addEventListener('keydown', this._onKeyDown, true); | ||
documentElement.addEventListener('touchend', this._onTouchEnd, true); | ||
documentElement.addEventListener('mousedown', this._onMouseDown, true); | ||
// passive events - doesn't call `preventDefault` or `stopPropagation` | ||
documentElement.addEventListener('click', this._onClick, { passive: true }); | ||
documentElement.addEventListener('touchmove', this._onTouchMove, { | ||
passive: true, | ||
}); | ||
documentElement.addEventListener('mouseover', this._onMouseOver, { | ||
passive: true, | ||
}); | ||
if (this._isSelectOneElement) { | ||
this.containerOuter.element.addEventListener('focus', this._onFocus); | ||
this.containerOuter.element.addEventListener('blur', this._onBlur); | ||
this.containerOuter.element.addEventListener('focus', this._onFocus, { | ||
passive: true, | ||
}); | ||
this.containerOuter.element.addEventListener('blur', this._onBlur, { | ||
passive: true, | ||
}); | ||
} | ||
this.input.element.addEventListener('focus', this._onFocus); | ||
this.input.element.addEventListener('blur', this._onBlur); | ||
this.input.element.addEventListener('keyup', this._onKeyUp, { | ||
passive: true, | ||
}); | ||
this.input.element.addEventListener('focus', this._onFocus, { | ||
passive: true, | ||
}); | ||
this.input.element.addEventListener('blur', this._onBlur, { | ||
passive: true, | ||
}); | ||
if (this.input.element.form) { | ||
this.input.element.form.addEventListener('reset', this._onFormReset); | ||
this.input.element.form.addEventListener('reset', this._onFormReset, { | ||
passive: true, | ||
}); | ||
} | ||
@@ -1146,20 +1283,41 @@ | ||
_removeEventListeners() { | ||
window.delegateEvent.remove('keyup', this._onKeyUp); | ||
window.delegateEvent.remove('keydown', this._onKeyDown); | ||
window.delegateEvent.remove('click', this._onClick); | ||
window.delegateEvent.remove('touchmove', this._onTouchMove); | ||
window.delegateEvent.remove('touchend', this._onTouchEnd); | ||
window.delegateEvent.remove('mousedown', this._onMouseDown); | ||
window.delegateEvent.remove('mouseover', this._onMouseOver); | ||
const { documentElement } = document; | ||
documentElement.removeEventListener('keydown', this._onKeyDown, true); | ||
documentElement.removeEventListener('touchend', this._onTouchEnd, true); | ||
documentElement.removeEventListener('mousedown', this._onMouseDown, true); | ||
documentElement.removeEventListener('keyup', this._onKeyUp, { | ||
passive: true, | ||
}); | ||
documentElement.removeEventListener('click', this._onClick, { | ||
passive: true, | ||
}); | ||
documentElement.removeEventListener('touchmove', this._onTouchMove, { | ||
passive: true, | ||
}); | ||
documentElement.removeEventListener('mouseover', this._onMouseOver, { | ||
passive: true, | ||
}); | ||
if (this._isSelectOneElement) { | ||
this.containerOuter.element.removeEventListener('focus', this._onFocus); | ||
this.containerOuter.element.removeEventListener('blur', this._onBlur); | ||
this.containerOuter.element.removeEventListener('focus', this._onFocus, { | ||
passive: true, | ||
}); | ||
this.containerOuter.element.removeEventListener('blur', this._onBlur, { | ||
passive: true, | ||
}); | ||
} | ||
this.input.element.removeEventListener('focus', this._onFocus); | ||
this.input.element.removeEventListener('blur', this._onBlur); | ||
this.input.element.removeEventListener('focus', this._onFocus, { | ||
passive: true, | ||
}); | ||
this.input.element.removeEventListener('blur', this._onBlur, { | ||
passive: true, | ||
}); | ||
if (this.input.element.form) { | ||
this.input.element.form.removeEventListener('reset', this._onFormReset); | ||
this.input.element.form.removeEventListener('reset', this._onFormReset, { | ||
passive: true, | ||
}); | ||
} | ||
@@ -1183,3 +1341,3 @@ | ||
const hasActiveDropdown = this.dropdown.isActive; | ||
const hasItems = this.itemList.hasChildren; | ||
const hasItems = this.itemList.hasChildren(); | ||
const keyString = String.fromCharCode(keyCode); | ||
@@ -1235,6 +1393,2 @@ | ||
_onKeyUp({ target, keyCode }) { | ||
if (target !== this.input.element) { | ||
return; | ||
} | ||
const { value } = this.input; | ||
@@ -1442,3 +1596,6 @@ const { activeItems } = this._store; | ||
// If we have our mouse down on the scrollbar and are on IE11... | ||
if (this.choiceList.element.contains(target) && isIE11()) { | ||
if ( | ||
this.choiceList.element.contains(target) && | ||
isIE11(navigator.userAgent) | ||
) { | ||
this._isScrollingOnIe = true; | ||
@@ -1660,3 +1817,3 @@ } | ||
}) { | ||
let passedValue = isType('String', value) ? value.trim() : value; | ||
let passedValue = typeof value === 'string' ? value.trim() : value; | ||
@@ -1823,4 +1980,5 @@ const passedKeyCode = keyCode; | ||
const { templates, classNames } = this.config; | ||
return templates[template].call(this, classNames, ...args); | ||
const { classNames } = this.config; | ||
return this._templates[template].call(this, classNames, ...args); | ||
} | ||
@@ -1834,3 +1992,3 @@ | ||
callbackOnCreateTemplates && | ||
isType('Function', callbackOnCreateTemplates) | ||
typeof callbackOnCreateTemplates === 'function' | ||
) { | ||
@@ -1840,3 +1998,3 @@ userTemplates = callbackOnCreateTemplates.call(this, strToEl); | ||
this.config.templates = merge(TEMPLATES, userTemplates); | ||
this._templates = merge(TEMPLATES, userTemplates); | ||
} | ||
@@ -1870,2 +2028,3 @@ | ||
type: this.passedElement.element.type, | ||
preventPaste: !this.config.paste, | ||
}); | ||
@@ -1974,3 +2133,5 @@ | ||
// If sorting is enabled or the user is searching, filter choices | ||
if (this.config.shouldSort) allChoices.sort(filter); | ||
if (this.config.shouldSort) { | ||
allChoices.sort(filter); | ||
} | ||
@@ -2115,12 +2276,2 @@ // Determine whether there is a selected choice | ||
_generateInstances(elements, config) { | ||
return elements.reduce( | ||
(instances, element) => { | ||
instances.push(new Choices(element, config)); | ||
return instances; | ||
}, | ||
[this], | ||
); | ||
} | ||
_generatePlaceholderValue() { | ||
@@ -2140,4 +2291,2 @@ if (this._isSelectOneElement) { | ||
Choices.userDefaults = {}; | ||
export default Choices; |
@@ -18,2 +18,3 @@ export default class Dropdown { | ||
); | ||
return this.position; | ||
@@ -39,2 +40,3 @@ } | ||
this.isActive = true; | ||
return this; | ||
@@ -52,4 +54,5 @@ } | ||
this.isActive = false; | ||
return this; | ||
} | ||
} |
@@ -1,10 +0,18 @@ | ||
import { calcWidthOfInput, sanitise } from '../lib/utils'; | ||
import { sanitise } from '../lib/utils'; | ||
export default class Input { | ||
constructor({ element, type, classNames, placeholderValue }) { | ||
Object.assign(this, { element, type, classNames, placeholderValue }); | ||
/** | ||
* | ||
* @typedef {import('../../../types/index').Choices.passedElement} passedElement | ||
* @typedef {import('../../../types/index').Choices.ClassNames} ClassNames | ||
* @param {{element: HTMLInputElement, type: passedElement['type'], classNames: ClassNames, preventPaste: boolean }} p | ||
*/ | ||
constructor({ element, type, classNames, preventPaste }) { | ||
this.element = element; | ||
this.type = type; | ||
this.classNames = classNames; | ||
this.preventPaste = preventPaste; | ||
this.isFocussed = this.element === document.activeElement; | ||
this.isDisabled = false; | ||
this.isDisabled = element.disabled; | ||
this._onPaste = this._onPaste.bind(this); | ||
@@ -20,2 +28,6 @@ this._onInput = this._onInput.bind(this); | ||
get value() { | ||
return sanitise(this.element.value); | ||
} | ||
set value(value) { | ||
@@ -25,26 +37,26 @@ this.element.value = value; | ||
get value() { | ||
return sanitise(this.element.value); | ||
} | ||
addEventListeners() { | ||
this.element.addEventListener('input', this._onInput); | ||
this.element.addEventListener('paste', this._onPaste); | ||
this.element.addEventListener('focus', this._onFocus); | ||
this.element.addEventListener('blur', this._onBlur); | ||
if (this.element.form) { | ||
this.element.form.addEventListener('reset', this._onFormReset); | ||
} | ||
this.element.addEventListener('input', this._onInput, { | ||
passive: true, | ||
}); | ||
this.element.addEventListener('focus', this._onFocus, { | ||
passive: true, | ||
}); | ||
this.element.addEventListener('blur', this._onBlur, { | ||
passive: true, | ||
}); | ||
} | ||
removeEventListeners() { | ||
this.element.removeEventListener('input', this._onInput); | ||
this.element.removeEventListener('input', this._onInput, { | ||
passive: true, | ||
}); | ||
this.element.removeEventListener('paste', this._onPaste); | ||
this.element.removeEventListener('focus', this._onFocus); | ||
this.element.removeEventListener('blur', this._onBlur); | ||
if (this.element.form) { | ||
this.element.form.removeEventListener('reset', this._onFormReset); | ||
} | ||
this.element.removeEventListener('focus', this._onFocus, { | ||
passive: true, | ||
}); | ||
this.element.removeEventListener('blur', this._onBlur, { | ||
passive: true, | ||
}); | ||
} | ||
@@ -94,28 +106,10 @@ | ||
* value or input value | ||
* @return | ||
*/ | ||
setWidth(enforceWidth) { | ||
const callback = width => { | ||
this.element.style.width = width; | ||
}; | ||
if (this._placeholderValue) { | ||
// If there is a placeholder, we only want to set the width of the input when it is a greater | ||
// length than 75% of the placeholder. This stops the input jumping around. | ||
const valueHasDesiredLength = | ||
this.element.value.length >= this._placeholderValue.length / 1.25; | ||
if ((this.element.value && valueHasDesiredLength) || enforceWidth) { | ||
this.calcWidth(callback); | ||
} | ||
} else { | ||
// If there is no placeholder, resize input to contents | ||
this.calcWidth(callback); | ||
} | ||
setWidth() { | ||
// Resize input to contents or placeholder | ||
const { style, value, placeholder } = this.element; | ||
style.minWidth = `${placeholder.length + 1}ch`; | ||
style.width = `${value.length + 1}ch`; | ||
} | ||
calcWidth(callback) { | ||
return calcWidthOfInput(this.element, callback); | ||
} | ||
setActiveDescendant(activeDescendantID) { | ||
@@ -136,4 +130,3 @@ this.element.setAttribute('aria-activedescendant', activeDescendantID); | ||
_onPaste(event) { | ||
const { target } = event; | ||
if (target === this.element && this.preventPaste) { | ||
if (this.preventPaste) { | ||
event.preventDefault(); | ||
@@ -140,0 +133,0 @@ } |
@@ -9,3 +9,2 @@ import { SCROLLING_SPEED } from '../constants'; | ||
this.height = this.element.offsetHeight; | ||
this.hasChildren = !!this.element.children; | ||
} | ||
@@ -25,2 +24,6 @@ | ||
hasChildren() { | ||
return this.element.hasChildNodes(); | ||
} | ||
scrollToTop() { | ||
@@ -42,3 +45,3 @@ this.element.scrollTop = 0; | ||
// Difference between the choice and scroll position | ||
const endpoint = | ||
const destination = | ||
direction > 0 | ||
@@ -49,8 +52,8 @@ ? this.element.scrollTop + choicePos - containerScrollPos | ||
requestAnimationFrame(time => { | ||
this._animateScroll(time, endpoint, direction); | ||
this._animateScroll(time, destination, direction); | ||
}); | ||
} | ||
_scrollDown(scrollPos, strength, endpoint) { | ||
const easing = (endpoint - scrollPos) / strength; | ||
_scrollDown(scrollPos, strength, destination) { | ||
const easing = (destination - scrollPos) / strength; | ||
const distance = easing > 1 ? easing : 1; | ||
@@ -61,4 +64,4 @@ | ||
_scrollUp(scrollPos, strength, endpoint) { | ||
const easing = (scrollPos - endpoint) / strength; | ||
_scrollUp(scrollPos, strength, destination) { | ||
const easing = (scrollPos - destination) / strength; | ||
const distance = easing > 1 ? easing : 1; | ||
@@ -69,3 +72,3 @@ | ||
_animateScroll(time, endpoint, direction) { | ||
_animateScroll(time, destination, direction) { | ||
const strength = SCROLLING_SPEED; | ||
@@ -76,11 +79,11 @@ const choiceListScrollTop = this.element.scrollTop; | ||
if (direction > 0) { | ||
this._scrollDown(choiceListScrollTop, strength, endpoint); | ||
this._scrollDown(choiceListScrollTop, strength, destination); | ||
if (choiceListScrollTop < endpoint) { | ||
if (choiceListScrollTop < destination) { | ||
continueAnimation = true; | ||
} | ||
} else { | ||
this._scrollUp(choiceListScrollTop, strength, endpoint); | ||
this._scrollUp(choiceListScrollTop, strength, destination); | ||
if (choiceListScrollTop > endpoint) { | ||
if (choiceListScrollTop > destination) { | ||
continueAnimation = true; | ||
@@ -92,3 +95,3 @@ } | ||
requestAnimationFrame(() => { | ||
this._animateScroll(time, endpoint, direction); | ||
this._animateScroll(time, destination, direction); | ||
}); | ||
@@ -95,0 +98,0 @@ } |
@@ -1,2 +0,2 @@ | ||
import { dispatchEvent, isElement } from '../lib/utils'; | ||
import { dispatchEvent } from '../lib/utils'; | ||
@@ -7,3 +7,3 @@ export default class WrappedElement { | ||
if (!isElement(element)) { | ||
if (!(element instanceof Element)) { | ||
throw new TypeError('Invalid element passed'); | ||
@@ -27,3 +27,3 @@ } | ||
this.element.classList.add(this.classNames.input); | ||
this.element.classList.add(this.classNames.hiddenState); | ||
this.element.hidden = true; | ||
@@ -40,3 +40,2 @@ // Remove element from tab index | ||
this.element.setAttribute('aria-hidden', 'true'); | ||
this.element.setAttribute('data-choice', 'active'); | ||
@@ -48,3 +47,3 @@ } | ||
this.element.classList.remove(this.classNames.input); | ||
this.element.classList.remove(this.classNames.hiddenState); | ||
this.element.hidden = false; | ||
this.element.removeAttribute('tabindex'); | ||
@@ -61,7 +60,7 @@ | ||
} | ||
this.element.removeAttribute('aria-hidden'); | ||
this.element.removeAttribute('data-choice'); | ||
// Re-assign values - this is weird, I know | ||
this.element.value = this.element.value; | ||
// @todo Figure out why we need to do this | ||
this.element.value = this.element.value; // eslint-disable-line no-self-assign | ||
} | ||
@@ -68,0 +67,0 @@ |
@@ -9,2 +9,6 @@ import WrappedElement from './wrapped-element'; | ||
get value() { | ||
return this.element.value; | ||
} | ||
set value(items) { | ||
@@ -17,6 +21,2 @@ const itemValues = items.map(({ value }) => value); | ||
} | ||
get value() { | ||
return this.element.value; | ||
} | ||
} |
@@ -25,3 +25,2 @@ import { sanitise, sortByAlpha } from './lib/utils'; | ||
highlightedState: 'is-highlighted', | ||
hiddenState: 'is-hidden', | ||
flippedState: 'is-flipped', | ||
@@ -40,3 +39,3 @@ loadingState: 'is-loading', | ||
addItems: true, | ||
addItemFilterFn: null, | ||
addItemFilter: null, | ||
removeItems: true, | ||
@@ -43,0 +42,0 @@ removeItemButton: false, |
@@ -13,3 +13,7 @@ window.delegateEvent = (function delegateEvent() { | ||
const type = events.get(event.type); | ||
if (!type) return; | ||
if (!type) { | ||
return; | ||
} | ||
type.forEach(fn => fn(event)); | ||
@@ -33,3 +37,5 @@ } | ||
remove: function remove(type, fn) { | ||
if (!events.get(type)) return; | ||
if (!events.get(type)) { | ||
return; | ||
} | ||
events.set(type, events.get(type).filter(item => item !== fn)); | ||
@@ -36,0 +42,0 @@ if (!events.get(type).length) { |
@@ -31,4 +31,2 @@ export const getRandomNumber = (min, max) => | ||
export const isElement = element => element instanceof Element; | ||
export const wrap = (element, wrapper = document.createElement('div')) => { | ||
@@ -40,19 +38,12 @@ if (element.nextSibling) { | ||
} | ||
return wrapper.appendChild(element); | ||
}; | ||
export const findAncestorByAttrName = (el, attr) => { | ||
let target = el; | ||
/** | ||
* @param {HTMLElement} el | ||
* @param {string} attr | ||
*/ | ||
export const findAncestorByAttrName = (el, attr) => el.closest(`[${attr}]`); | ||
while (target) { | ||
if (target.hasAttribute(attr)) { | ||
return target; | ||
} | ||
target = target.parentElement; | ||
} | ||
return null; | ||
}; | ||
export const getAdjacentEl = (startEl, className, direction = 1) => { | ||
@@ -92,3 +83,3 @@ if (!startEl || !className) { | ||
export const sanitise = value => { | ||
if (!isType('String', value)) { | ||
if (typeof value !== 'string') { | ||
return value; | ||
@@ -106,2 +97,3 @@ } | ||
const tmpEl = document.createElement('div'); | ||
return str => { | ||
@@ -120,65 +112,15 @@ const cleanedInput = str.trim(); | ||
/** | ||
* Determines the width of a passed input based on its value and passes | ||
* it to the supplied callback function. | ||
*/ | ||
export const calcWidthOfInput = (input, callback) => { | ||
const value = input.value || input.placeholder; | ||
let width = input.offsetWidth; | ||
if (value) { | ||
const testEl = strToEl(`<span>${sanitise(value)}</span>`); | ||
testEl.style.position = 'absolute'; | ||
testEl.style.padding = '0'; | ||
testEl.style.top = '-9999px'; | ||
testEl.style.left = '-9999px'; | ||
testEl.style.width = 'auto'; | ||
testEl.style.whiteSpace = 'pre'; | ||
if (document.body.contains(input) && window.getComputedStyle) { | ||
const inputStyle = window.getComputedStyle(input); | ||
if (inputStyle) { | ||
testEl.style.fontSize = inputStyle.fontSize; | ||
testEl.style.fontFamily = inputStyle.fontFamily; | ||
testEl.style.fontWeight = inputStyle.fontWeight; | ||
testEl.style.fontStyle = inputStyle.fontStyle; | ||
testEl.style.letterSpacing = inputStyle.letterSpacing; | ||
testEl.style.textTransform = inputStyle.textTransform; | ||
testEl.style.paddingLeft = inputStyle.paddingLeft; | ||
testEl.style.paddingRight = inputStyle.paddingRight; | ||
} | ||
} | ||
document.body.appendChild(testEl); | ||
requestAnimationFrame(() => { | ||
if (value && testEl.offsetWidth !== input.offsetWidth) { | ||
width = testEl.offsetWidth + 4; | ||
} | ||
document.body.removeChild(testEl); | ||
callback.call(this, `${width}px`); | ||
export const sortByAlpha = | ||
/** | ||
* @param {{ label?: string, value: string }} a | ||
* @param {{ label?: string, value: string }} b | ||
* @returns {number} | ||
*/ | ||
({ value, label = value }, { value: value2, label: label2 = value2 }) => | ||
label.localeCompare(label2, [], { | ||
sensitivity: 'base', | ||
ignorePunctuation: true, | ||
numeric: true, | ||
}); | ||
} else { | ||
callback.call(this, `${width}px`); | ||
} | ||
}; | ||
export const sortByAlpha = (a, b) => { | ||
const labelA = `${a.label || a.value}`.toLowerCase(); | ||
const labelB = `${b.label || b.value}`.toLowerCase(); | ||
if (labelA < labelB) { | ||
return -1; | ||
} | ||
if (labelA > labelB) { | ||
return 1; | ||
} | ||
return 0; | ||
}; | ||
export const sortByScore = (a, b) => a.score - b.score; | ||
@@ -199,2 +141,3 @@ | ||
const html = document.documentElement; | ||
return Math.max( | ||
@@ -209,24 +152,8 @@ body.scrollHeight, | ||
export const fetchFromObject = (object, path) => { | ||
const index = path.indexOf('.'); | ||
export const isIE11 = userAgent => | ||
!!(userAgent.match(/Trident/) && userAgent.match(/rv[ :]11/)); | ||
if (index > -1) { | ||
return fetchFromObject( | ||
object[path.substring(0, index)], | ||
path.substr(index + 1), | ||
); | ||
} | ||
return object[path]; | ||
}; | ||
export const isIE11 = () => | ||
!!( | ||
navigator.userAgent.match(/Trident/) && | ||
navigator.userAgent.match(/rv[ :]11/) | ||
); | ||
export const existsInArray = (array, value, key = 'value') => | ||
array.some(item => { | ||
if (isType('String', value)) { | ||
if (typeof value === 'string') { | ||
return item[key] === value.trim(); | ||
@@ -233,0 +160,0 @@ } |
@@ -36,2 +36,3 @@ export const defaultState = []; | ||
choice.active = action.active; | ||
return choice; | ||
@@ -49,2 +50,3 @@ }); | ||
} | ||
return choice; | ||
@@ -66,2 +68,3 @@ }); | ||
} | ||
return choice; | ||
@@ -82,4 +85,6 @@ }); | ||
choice.score = score; | ||
return true; | ||
} | ||
return false; | ||
@@ -96,2 +101,3 @@ }); | ||
choice.active = action.active; | ||
return choice; | ||
@@ -98,0 +104,0 @@ }); |
@@ -26,2 +26,3 @@ export const defaultState = []; | ||
item.highlighted = false; | ||
return item; | ||
@@ -38,2 +39,3 @@ }); | ||
} | ||
return item; | ||
@@ -49,2 +51,3 @@ }); | ||
} | ||
return item; | ||
@@ -51,0 +54,0 @@ }); |
@@ -128,2 +128,3 @@ import { createStore } from 'redux'; | ||
); | ||
return isActive && hasActiveOptions; | ||
@@ -143,8 +144,12 @@ }, []); | ||
* Get single choice by it's ID | ||
* @return {Object} Found choice | ||
* @param {id} string | ||
* @return {import('../../../types/index').Choices.Choice | false} Found choice | ||
*/ | ||
getChoiceById(id) { | ||
if (id) { | ||
return this.activeChoices.find(choice => choice.id === parseInt(id, 10)); | ||
const n = parseInt(id, 10); | ||
return this.activeChoices.find(choice => choice.id === n); | ||
} | ||
return false; | ||
@@ -151,0 +156,0 @@ } |
@@ -1,8 +0,11 @@ | ||
import classNames from 'classnames'; | ||
import { strToEl } from './lib/utils'; | ||
/** | ||
* Helpers to create HTML elements used by Choices | ||
* Can be overridden by providing `callbackOnCreateTemplates` option | ||
* @typedef {import('../../types/index').Choices.Templates} Templates | ||
*/ | ||
export const TEMPLATES = { | ||
export const TEMPLATES = /** @type {Templates} */ ({ | ||
containerOuter( | ||
globalClasses, | ||
direction, | ||
{ containerOuter }, | ||
dir, | ||
isSelectElement, | ||
@@ -13,228 +16,234 @@ isSelectOneElement, | ||
) { | ||
const tabIndex = isSelectOneElement ? 'tabindex="0"' : ''; | ||
let role = isSelectElement ? 'role="listbox"' : ''; | ||
let ariaAutoComplete = ''; | ||
const div = Object.assign(document.createElement('div'), { | ||
className: containerOuter, | ||
}); | ||
if (isSelectElement && searchEnabled) { | ||
role = 'role="combobox"'; | ||
ariaAutoComplete = 'aria-autocomplete="list"'; | ||
div.dataset.type = passedElementType; | ||
if (dir) { | ||
div.dir = dir; | ||
} | ||
return strToEl(` | ||
<div | ||
class="${globalClasses.containerOuter}" | ||
data-type="${passedElementType}" | ||
${role} | ||
${tabIndex} | ||
${ariaAutoComplete} | ||
aria-haspopup="true" | ||
aria-expanded="false" | ||
dir="${direction}" | ||
> | ||
</div> | ||
`); | ||
if (isSelectOneElement) { | ||
div.tabIndex = 0; | ||
} | ||
if (isSelectElement) { | ||
div.setAttribute('role', searchEnabled ? 'combobox' : 'listbox'); | ||
if (searchEnabled) { | ||
div.setAttribute('aria-autocomplete', 'list'); | ||
} | ||
} | ||
div.setAttribute('aria-haspopup', 'true'); | ||
div.setAttribute('aria-expanded', 'false'); | ||
return div; | ||
}, | ||
containerInner(globalClasses) { | ||
return strToEl(` | ||
<div class="${globalClasses.containerInner}"></div> | ||
`); | ||
containerInner({ containerInner }) { | ||
return Object.assign(document.createElement('div'), { | ||
className: containerInner, | ||
}); | ||
}, | ||
itemList(globalClasses, isSelectOneElement) { | ||
const localClasses = classNames(globalClasses.list, { | ||
[globalClasses.listSingle]: isSelectOneElement, | ||
[globalClasses.listItems]: !isSelectOneElement, | ||
itemList({ list, listSingle, listItems }, isSelectOneElement) { | ||
return Object.assign(document.createElement('div'), { | ||
className: `${list} ${isSelectOneElement ? listSingle : listItems}`, | ||
}); | ||
return strToEl(` | ||
<div class="${localClasses}"></div> | ||
`); | ||
}, | ||
placeholder(globalClasses, value) { | ||
return strToEl(` | ||
<div class="${globalClasses.placeholder}"> | ||
${value} | ||
</div> | ||
`); | ||
placeholder({ placeholder }, value) { | ||
return Object.assign(document.createElement('div'), { | ||
className: placeholder, | ||
innerHTML: value, | ||
}); | ||
}, | ||
item(globalClasses, data, removeItemButton) { | ||
const ariaSelected = data.active ? 'aria-selected="true"' : ''; | ||
const ariaDisabled = data.disabled ? 'aria-disabled="true"' : ''; | ||
let localClasses = classNames(globalClasses.item, { | ||
[globalClasses.highlightedState]: data.highlighted, | ||
[globalClasses.itemSelectable]: !data.highlighted, | ||
[globalClasses.placeholder]: data.placeholder, | ||
item( | ||
{ item, button, highlightedState, itemSelectable, placeholder }, | ||
{ | ||
id, | ||
value, | ||
label, | ||
customProperties, | ||
active, | ||
disabled, | ||
highlighted, | ||
placeholder: isPlaceholder, | ||
}, | ||
removeItemButton, | ||
) { | ||
const div = Object.assign(document.createElement('div'), { | ||
className: item, | ||
innerHTML: label, | ||
}); | ||
Object.assign(div.dataset, { | ||
item: '', | ||
id, | ||
value, | ||
customProperties, | ||
}); | ||
if (active) { | ||
div.setAttribute('aria-selected', 'true'); | ||
} | ||
if (disabled) { | ||
div.setAttribute('aria-disabled', 'true'); | ||
} | ||
if (isPlaceholder) { | ||
div.classList.add(placeholder); | ||
} | ||
div.classList.add(highlighted ? highlightedState : itemSelectable); | ||
if (removeItemButton) { | ||
localClasses = classNames(globalClasses.item, { | ||
[globalClasses.highlightedState]: data.highlighted, | ||
[globalClasses.itemSelectable]: !data.disabled, | ||
[globalClasses.placeholder]: data.placeholder, | ||
if (disabled) { | ||
div.classList.remove(itemSelectable); | ||
} | ||
div.dataset.deletable = ''; | ||
/** @todo This MUST be localizable, not hardcoded! */ | ||
const REMOVE_ITEM_TEXT = 'Remove item'; | ||
const removeButton = Object.assign(document.createElement('button'), { | ||
type: 'button', | ||
className: button, | ||
innerHTML: REMOVE_ITEM_TEXT, | ||
}); | ||
removeButton.setAttribute( | ||
'aria-label', | ||
`${REMOVE_ITEM_TEXT}: '${value}'`, | ||
); | ||
removeButton.dataset.button = ''; | ||
div.appendChild(removeButton); | ||
} | ||
return strToEl(` | ||
<div | ||
class="${localClasses}" | ||
data-item | ||
data-id="${data.id}" | ||
data-value="${data.value}" | ||
data-custom-properties='${data.customProperties}' | ||
data-deletable | ||
${ariaSelected} | ||
${ariaDisabled} | ||
> | ||
${data.label}<!-- | ||
--><button | ||
type="button" | ||
class="${globalClasses.button}" | ||
data-button | ||
aria-label="Remove item: '${data.value}'" | ||
> | ||
Remove item | ||
</button> | ||
</div> | ||
`); | ||
return div; | ||
}, | ||
choiceList({ list }, isSelectOneElement) { | ||
const div = Object.assign(document.createElement('div'), { | ||
className: list, | ||
}); | ||
if (!isSelectOneElement) { | ||
div.setAttribute('aria-multiselectable', 'true'); | ||
} | ||
div.setAttribute('role', 'listbox'); | ||
return strToEl(` | ||
<div | ||
class="${localClasses}" | ||
data-item | ||
data-id="${data.id}" | ||
data-value="${data.value}" | ||
${ariaSelected} | ||
${ariaDisabled} | ||
> | ||
${data.label} | ||
</div> | ||
`); | ||
return div; | ||
}, | ||
choiceList(globalClasses, isSelectOneElement) { | ||
const ariaMultiSelectable = !isSelectOneElement | ||
? 'aria-multiselectable="true"' | ||
: ''; | ||
return strToEl(` | ||
<div | ||
class="${globalClasses.list}" | ||
dir="ltr" | ||
role="listbox" | ||
${ariaMultiSelectable} | ||
> | ||
</div> | ||
`); | ||
}, | ||
choiceGroup(globalClasses, data) { | ||
const ariaDisabled = data.disabled ? 'aria-disabled="true"' : ''; | ||
const localClasses = classNames(globalClasses.group, { | ||
[globalClasses.itemDisabled]: data.disabled, | ||
choiceGroup({ group, groupHeading, itemDisabled }, { id, value, disabled }) { | ||
const div = Object.assign(document.createElement('div'), { | ||
className: `${group} ${disabled ? itemDisabled : ''}`, | ||
}); | ||
return strToEl(` | ||
<div | ||
class="${localClasses}" | ||
data-group | ||
data-id="${data.id}" | ||
data-value="${data.value}" | ||
role="group" | ||
${ariaDisabled} | ||
> | ||
<div class="${globalClasses.groupHeading}">${data.value}</div> | ||
</div> | ||
`); | ||
}, | ||
choice(globalClasses, data, itemSelectText) { | ||
const role = data.groupId > 0 ? 'role="treeitem"' : 'role="option"'; | ||
const localClasses = classNames( | ||
globalClasses.item, | ||
globalClasses.itemChoice, | ||
{ | ||
[globalClasses.itemDisabled]: data.disabled, | ||
[globalClasses.itemSelectable]: !data.disabled, | ||
[globalClasses.placeholder]: data.placeholder, | ||
}, | ||
div.setAttribute('role', 'group'); | ||
Object.assign(div.dataset, { | ||
group: '', | ||
id, | ||
value, | ||
}); | ||
if (disabled) { | ||
div.setAttribute('aria-disabled', 'true'); | ||
} | ||
div.appendChild( | ||
Object.assign(document.createElement('div'), { | ||
className: groupHeading, | ||
innerHTML: value, | ||
}), | ||
); | ||
return strToEl(` | ||
<div | ||
class="${localClasses}" | ||
data-select-text="${itemSelectText}" | ||
data-choice | ||
data-id="${data.id}" | ||
data-value="${data.value}" | ||
${ | ||
data.disabled | ||
? 'data-choice-disabled aria-disabled="true"' | ||
: 'data-choice-selectable' | ||
} | ||
id="${data.elementId}" | ||
${role} | ||
> | ||
${data.label} | ||
</div> | ||
`); | ||
return div; | ||
}, | ||
input(globalClasses, placeholderValue) { | ||
const localClasses = classNames( | ||
globalClasses.input, | ||
globalClasses.inputCloned, | ||
); | ||
return strToEl(` | ||
<input | ||
type="text" | ||
class="${localClasses}" | ||
autocomplete="off" | ||
autocapitalize="off" | ||
spellcheck="false" | ||
role="textbox" | ||
aria-autocomplete="list" | ||
aria-label="${placeholderValue}" | ||
> | ||
`); | ||
choice( | ||
{ item, itemChoice, itemSelectable, itemDisabled, placeholder }, | ||
{ | ||
id, | ||
value, | ||
label, | ||
groupId, | ||
elementId, | ||
disabled, | ||
placeholder: isPlaceholder, | ||
}, | ||
selectText, | ||
) { | ||
const div = Object.assign(document.createElement('div'), { | ||
id: elementId, | ||
innerHTML: label, | ||
className: `${item} ${itemChoice} ${ | ||
disabled ? itemDisabled : itemSelectable | ||
} ${isPlaceholder ? placeholder : ''}`, | ||
}); | ||
div.setAttribute('role', groupId > 0 ? 'treeitem' : 'option'); | ||
Object.assign(div.dataset, { | ||
choice: '', | ||
id, | ||
value, | ||
selectText, | ||
}); | ||
if (disabled) { | ||
div.dataset.choiceDisabled = ''; | ||
div.setAttribute('aria-disabled', 'true'); | ||
} else { | ||
div.dataset.choiceSelectable = ''; | ||
} | ||
return div; | ||
}, | ||
dropdown(globalClasses) { | ||
const localClasses = classNames( | ||
globalClasses.list, | ||
globalClasses.listDropdown, | ||
); | ||
input({ input, inputCloned }, placeholderValue) { | ||
const inp = Object.assign(document.createElement('input'), { | ||
type: 'text', | ||
className: `${input} ${inputCloned}`, | ||
autocomplete: 'off', | ||
autocapitalize: 'off', | ||
spellcheck: false, | ||
}); | ||
return strToEl(` | ||
<div | ||
class="${localClasses}" | ||
aria-expanded="false" | ||
> | ||
</div> | ||
`); | ||
inp.setAttribute('role', 'textbox'); | ||
inp.setAttribute('aria-autocomplete', 'list'); | ||
inp.setAttribute('aria-label', placeholderValue); | ||
return inp; | ||
}, | ||
notice(globalClasses, label, type = '') { | ||
const localClasses = classNames( | ||
globalClasses.item, | ||
globalClasses.itemChoice, | ||
{ | ||
[globalClasses.noResults]: type === 'no-results', | ||
[globalClasses.noChoices]: type === 'no-choices', | ||
}, | ||
); | ||
dropdown({ list, listDropdown }) { | ||
const div = document.createElement('div'); | ||
return strToEl(` | ||
<div class="${localClasses}"> | ||
${label} | ||
</div> | ||
`); | ||
div.classList.add(list, listDropdown); | ||
div.setAttribute('aria-expanded', 'false'); | ||
return div; | ||
}, | ||
option(data) { | ||
return strToEl(` | ||
<option value="${data.value}" ${data.active ? 'selected' : ''} ${ | ||
data.disabled ? 'disabled' : '' | ||
} ${ | ||
data.customProperties | ||
? `data-custom-properties=${data.customProperties}` | ||
: '' | ||
}>${data.label}</option> | ||
`); | ||
notice({ item, itemChoice, noResults, noChoices }, innerHTML, type = '') { | ||
const classes = [item, itemChoice]; | ||
if (type === 'no-choices') { | ||
classes.push(noChoices); | ||
} else if (type === 'no-results') { | ||
classes.push(noResults); | ||
} | ||
return Object.assign(document.createElement('div'), { | ||
innerHTML, | ||
className: classes.join(' '), | ||
}); | ||
}, | ||
}; | ||
option({ label, value, customProperties, active, disabled }) { | ||
const opt = new Option(label, value, false, active); | ||
if (customProperties) { | ||
opt.dataset.customProperties = customProperties; | ||
} | ||
opt.disabled = disabled; | ||
return opt; | ||
}, | ||
}); | ||
export default TEMPLATES; |
@@ -1,21 +0,26 @@ | ||
// Type definitions for Choices.js 7.0.0 | ||
// Type definitions for Choices.js 7.1.x | ||
// Project: https://github.com/jshjohnson/Choices | ||
// Definitions by: Arthur vasconcelos <https://github.com/arthurvasconcelos>, Josh Johnson <https://github.com/jshjohnson>, Zack Schuster <https://github.com/zackschuster> | ||
// Definitions by: | ||
// Arthur vasconcelos <https://github.com/arthurvasconcelos>, | ||
// Josh Johnson <https://github.com/jshjohnson>, | ||
// Zack Schuster <https://github.com/zackschuster> | ||
// Konstantin Vyatkin <https://github.com/tinovyatkin> | ||
// Definitions: https://github.com/jshjohnson/Choices | ||
// TypeScript Version: 2.9.2 | ||
import { FuseOptions } from 'fuse.js'; | ||
// Choices Namespace | ||
declare namespace Choices { | ||
namespace Types { | ||
type renderSelected = 'auto' | 'always'; | ||
type dropdownPosition = 'auto' | 'top'; | ||
type strToEl = (str: string) => HTMLElement | HTMLInputElement | HTMLOptionElement; | ||
type strToEl = ( | ||
str: string, | ||
) => HTMLElement | HTMLInputElement | HTMLOptionElement; | ||
type stringFunction = () => string; | ||
type noticeStringFunction = (value: string) => string; | ||
type noticeLimitFunction = (maxItemCount: number) => string; | ||
type callbackOnCreateTemplates = (template: strToEl) => Choices.Templates; | ||
type filterFunction = (value: string) => boolean; | ||
} | ||
interface Choice { | ||
customProperties?: { [prop: string]: any }; | ||
customProperties?: Record<string, any>; | ||
disabled?: boolean; | ||
@@ -27,5 +32,5 @@ elementId?: string; | ||
label: string; | ||
placeholder?: any; | ||
placeholder?: boolean; | ||
selected?: boolean; | ||
value: any; | ||
value: string; | ||
} | ||
@@ -44,14 +49,11 @@ | ||
*/ | ||
"addItem": CustomEvent; | ||
addItem: CustomEvent<{ | ||
id: string; | ||
value: string; | ||
label: string; | ||
groupValue: string; | ||
keyCode: string; | ||
}>; | ||
/** | ||
* A filter that will need to pass for a user to successfully add an item. | ||
* | ||
* **Input types affected:** text | ||
* | ||
* @default null | ||
*/ | ||
addItemFilterFn?: () => any; | ||
/** | ||
* Triggered each time an item is removed (programmatically or by the user). | ||
@@ -63,3 +65,8 @@ * | ||
*/ | ||
"removeItem": CustomEvent; | ||
removeItem: CustomEvent<{ | ||
id: string; | ||
value: string; | ||
label: string; | ||
groupValue: string; | ||
}>; | ||
@@ -73,3 +80,8 @@ /** | ||
*/ | ||
"highlightItem": CustomEvent; | ||
highlightItem: CustomEvent<{ | ||
id: string; | ||
value: string; | ||
label: string; | ||
groupValue: string; | ||
}>; | ||
@@ -83,3 +95,8 @@ /** | ||
*/ | ||
"unhighlightItem": CustomEvent; | ||
unhighlightItem: CustomEvent<{ | ||
id: string; | ||
value: string; | ||
label: string; | ||
groupValue: string; | ||
}>; | ||
@@ -91,5 +108,5 @@ /** | ||
* | ||
* Arguments: value, keyCode | ||
* Arguments: choice: Choice | ||
*/ | ||
"choice": CustomEvent; | ||
choice: CustomEvent<{ choice: Choices.Choice }>; | ||
@@ -103,3 +120,3 @@ /** | ||
*/ | ||
"change": CustomEvent; | ||
change: CustomEvent<{ value: string }>; | ||
@@ -113,3 +130,3 @@ /** | ||
*/ | ||
"search": CustomEvent; | ||
search: CustomEvent<{ value: string; resultCount: number }>; | ||
@@ -123,3 +140,3 @@ /** | ||
*/ | ||
"showDropdown": CustomEvent; | ||
showDropdown: CustomEvent<undefined>; | ||
@@ -133,3 +150,11 @@ /** | ||
*/ | ||
"hideDropdown": CustomEvent; | ||
hideDropdown: CustomEvent<undefined>; | ||
/** | ||
* Triggered when a choice from the dropdown is highlighted. | ||
* | ||
* Input types affected: select-one, select-multiple | ||
* Arguments: el is the choice.passedElement that was affected. | ||
*/ | ||
highlightChoice: CustomEvent<{ el: Choices.passedElement }>; | ||
} | ||
@@ -144,26 +169,63 @@ | ||
interface Item { | ||
interface Item extends Choice { | ||
choiceId?: string; | ||
customProperties?: { [prop: string]: any }; | ||
groupId?: string; | ||
id?: string; | ||
keyCode?: number; | ||
label: string; | ||
placeholder?: string; | ||
value: any; | ||
} | ||
interface Templates { | ||
containerOuter?: (classNames: ClassNames, direction: string) => HTMLElement; | ||
containerInner?: (classNames: ClassNames) => HTMLElement; | ||
itemList?: (classNames: ClassNames, isSelectOneElement: boolean) => HTMLElement; | ||
placeholder?: (classNames: ClassNames, value: string) => HTMLElement; | ||
item?: (classNames: ClassNames, data: any, removeItemButton: boolean) => HTMLElement; | ||
choiceList?: (classNames: ClassNames, isSelectOneElement: boolean) => HTMLElement; | ||
choiceGroup?: (classNames: ClassNames, data: any) => HTMLElement; | ||
choice?: (classNames: ClassNames, data: any) => HTMLElement; | ||
input?: (classNames: ClassNames) => HTMLInputElement; | ||
dropdown?: (classNames: ClassNames) => HTMLElement; | ||
notice?: (classNames: ClassNames, label: string) => HTMLElement; | ||
option?: (data: any) => HTMLOptionElement; | ||
containerOuter: ( | ||
this: Choices, | ||
classNames: ClassNames, | ||
direction: HTMLElement['dir'], | ||
isSelectElement: boolean, | ||
isSelectOneElement: boolean, | ||
searchEnabled: boolean, | ||
passedElementType: passedElement['type'], | ||
) => HTMLElement; | ||
containerInner: (this: Choices, classNames: ClassNames) => HTMLElement; | ||
itemList: ( | ||
this: Choices, | ||
classNames: ClassNames, | ||
isSelectOneElement: boolean, | ||
) => HTMLElement; | ||
placeholder: ( | ||
this: Choices, | ||
classNames: ClassNames, | ||
value: string, | ||
) => HTMLElement; | ||
item: ( | ||
this: Choices, | ||
classNames: ClassNames, | ||
data: Choice, | ||
removeItemButton: boolean, | ||
) => HTMLElement; | ||
choiceList: ( | ||
this: Choices, | ||
classNames: ClassNames, | ||
isSelectOneElement: boolean, | ||
) => HTMLElement; | ||
choiceGroup: ( | ||
this: Choices, | ||
classNames: ClassNames, | ||
data: Choice, | ||
) => HTMLElement; | ||
choice: ( | ||
this: Choices, | ||
classNames: ClassNames, | ||
data: Choice, | ||
selectText: string, | ||
) => HTMLElement; | ||
input: ( | ||
this: Choices, | ||
classNames: ClassNames, | ||
placeholderValue: string, | ||
) => HTMLInputElement; | ||
dropdown: (this: Choices, classNames: ClassNames) => HTMLElement; | ||
notice: ( | ||
this: Choices, | ||
classNames: ClassNames, | ||
label: string, | ||
type: '' | 'no-results' | 'no-choices', | ||
) => HTMLElement; | ||
option: (data: Choice) => HTMLOptionElement; | ||
} | ||
@@ -174,59 +236,68 @@ | ||
/** @default 'choices' */ | ||
containerOuter?: string; | ||
containerOuter: string; | ||
/** @default 'choices__inner' */ | ||
containerInner?: string; | ||
containerInner: string; | ||
/** @default 'choices__input' */ | ||
input?: string; | ||
input: string; | ||
/** @default 'choices__input--cloned' */ | ||
inputCloned?: string; | ||
inputCloned: string; | ||
/** @default 'choices__list' */ | ||
list?: string; | ||
list: string; | ||
/** @default 'choices__list--multiple' */ | ||
listItems?: string; | ||
listItems: string; | ||
/** @default 'choices__list--single' */ | ||
listSingle?: string; | ||
listSingle: string; | ||
/** @default 'choices__list--dropdown' */ | ||
listDropdown?: string; | ||
listDropdown: string; | ||
/** @default 'choices__item' */ | ||
item?: string; | ||
item: string; | ||
/** @default 'choices__item--selectable' */ | ||
itemSelectable?: string; | ||
itemSelectable: string; | ||
/** @default 'choices__item--disabled' */ | ||
itemDisabled?: string; | ||
itemDisabled: string; | ||
/** @default 'choices__item--choice' */ | ||
itemChoice?: string; | ||
itemChoice: string; | ||
/** @default 'choices__placeholder' */ | ||
placeholder?: string; | ||
placeholder: string; | ||
/** @default 'choices__group' */ | ||
group?: string; | ||
group: string; | ||
/** @default 'choices__heading' */ | ||
groupHeading?: string; | ||
groupHeading: string; | ||
/** @default 'choices__button' */ | ||
button?: string; | ||
button: string; | ||
/** @default 'is-active' */ | ||
activeState?: string; | ||
activeState: string; | ||
/** @default 'is-focused' */ | ||
focusState?: string; | ||
focusState: string; | ||
/** @default 'is-open' */ | ||
openState?: string; | ||
openState: string; | ||
/** @default 'is-disabled' */ | ||
disabledState?: string; | ||
disabledState: string; | ||
/** @default 'is-highlighted' */ | ||
highlightedState?: string; | ||
/** @default 'is-hidden' */ | ||
hiddenState?: string; | ||
highlightedState: string; | ||
/** @default 'is-flipped' */ | ||
flippedState?: string; | ||
flippedState: string; | ||
/** @default 'is-loading' */ | ||
loadingState?: string; | ||
loadingState: string; | ||
/** @default 'has-no-results' */ | ||
noResults?: string; | ||
noResults: string; | ||
/** @default 'has-no-choices' */ | ||
noChoices?: string; | ||
noChoices: string; | ||
} | ||
interface passedElement { | ||
classNames: Choices.ClassNames, | ||
element: HTMLElement, | ||
isDisabled: boolean, | ||
classNames: Choices.ClassNames; | ||
element: (HTMLInputElement | HTMLSelectElement) & { | ||
// Extends HTMLElement addEventListener with Choices events | ||
addEventListener<K extends keyof Choices.EventMap>( | ||
type: K, | ||
listener: ( | ||
this: HTMLInputElement | HTMLSelectElement, | ||
ev: Choices.EventMap[K], | ||
) => void, | ||
options?: boolean | AddEventListenerOptions, | ||
): void; | ||
}; | ||
type: 'text' | 'select-one' | 'select-multiple'; | ||
isDisabled: boolean; | ||
parentInstance: Choices; | ||
@@ -240,3 +311,3 @@ } | ||
* | ||
* - **Choice:** A choice is a value a user can select. A choice would be equivelant to the `<option></option>` element within a select input. | ||
* - **Choice:** A choice is a value a user can select. A choice would be equivalent to the `<option></option>` element within a select input. | ||
* - **Group:** A group is a collection of choices. A group should be seen as equivalent to a `<optgroup></optgroup>` element within a select input. | ||
@@ -253,3 +324,3 @@ * - **Item:** An item is an inputted value **_(text input)_** or a selected choice **_(select element)_**. In the context of a select element, an item is equivelent to a selected option element: `<option value="Hello" selected></option>` whereas in the context of a text input an item is equivelant to `<input type="text" value="Hello">` | ||
*/ | ||
silent?: boolean; | ||
silent: boolean; | ||
@@ -285,3 +356,3 @@ /** | ||
*/ | ||
items?: any[]; | ||
items: string[] | Choice[]; | ||
@@ -315,3 +386,3 @@ /** | ||
*/ | ||
choices?: any[]; | ||
choices: Choice[]; | ||
@@ -325,3 +396,3 @@ /** | ||
*/ | ||
renderChoiceLimit?: number; | ||
renderChoiceLimit: number; | ||
@@ -335,3 +406,3 @@ /** | ||
*/ | ||
maxItemCount?: number; | ||
maxItemCount: number; | ||
@@ -345,5 +416,26 @@ /** | ||
*/ | ||
addItems?: boolean; | ||
addItems: boolean; | ||
/** | ||
* A filter that will need to pass for a user to successfully add an item. | ||
* | ||
* **Input types affected:** text | ||
* | ||
* @default null | ||
*/ | ||
addItemFilter: string | RegExp | Choices.Types.filterFunction; | ||
/** | ||
* The text that is shown when a user has inputted a new item but has not pressed the enter key. To access the current input value, pass a function with a `value` argument (see the **default config** [https://github.com/jshjohnson/Choices#setup] for an example), otherwise pass a string. | ||
* | ||
* **Input types affected:** text | ||
* | ||
* @default | ||
* ``` | ||
* (value) => `Press Enter to add <b>"${value}"</b>`; | ||
* ``` | ||
*/ | ||
addItemText: string | Choices.Types.noticeStringFunction; | ||
/** | ||
* Whether a user can remove items. | ||
@@ -355,3 +447,3 @@ * | ||
*/ | ||
removeItems?: boolean; | ||
removeItems: boolean; | ||
@@ -365,3 +457,3 @@ /** | ||
*/ | ||
removeItemButton?: boolean; | ||
removeItemButton: boolean; | ||
@@ -375,3 +467,3 @@ /** | ||
*/ | ||
editItems?: boolean; | ||
editItems: boolean; | ||
@@ -385,6 +477,6 @@ /** | ||
*/ | ||
duplicateItemsAllowed?: boolean; | ||
duplicateItemsAllowed: boolean; | ||
/** | ||
* What divides each value. The default delimiter seperates each value with a comma: `"Value 1, Value 2, Value 3"`. | ||
* What divides each value. The default delimiter separates each value with a comma: `"Value 1, Value 2, Value 3"`. | ||
* | ||
@@ -395,3 +487,3 @@ * **Input types affected:** text | ||
*/ | ||
delimiter?: string; | ||
delimiter: string; | ||
@@ -405,3 +497,3 @@ /** | ||
*/ | ||
paste?: boolean; | ||
paste: boolean; | ||
@@ -417,3 +509,3 @@ /** | ||
*/ | ||
searchEnabled?: boolean; | ||
searchEnabled: boolean; | ||
@@ -427,3 +519,3 @@ /** | ||
*/ | ||
searchChoices?: boolean; | ||
searchChoices: boolean; | ||
@@ -437,3 +529,3 @@ /** | ||
*/ | ||
searchFloor?: number; | ||
searchFloor: number; | ||
@@ -447,3 +539,3 @@ /** | ||
*/ | ||
searchResultLimit?: number; | ||
searchResultLimit: number; | ||
@@ -457,3 +549,3 @@ /** | ||
*/ | ||
searchFields?: string[]; | ||
searchFields: string[]; | ||
@@ -467,3 +559,3 @@ /** | ||
*/ | ||
position?: Choices.Types.dropdownPosition; | ||
position: 'auto' | 'top'; | ||
@@ -477,3 +569,3 @@ /** | ||
*/ | ||
resetScrollPosition?: boolean; | ||
resetScrollPosition: boolean; | ||
@@ -487,3 +579,3 @@ /** | ||
*/ | ||
shouldSort?: boolean; | ||
shouldSort: boolean; | ||
@@ -497,3 +589,3 @@ /** | ||
*/ | ||
shouldSortItems?: boolean; | ||
shouldSortItems: boolean; | ||
@@ -517,3 +609,3 @@ /** | ||
*/ | ||
sortFilter?: (current: any, next: any) => number; | ||
sortFilter: (current: Choice, next: Choice) => number; | ||
@@ -537,3 +629,3 @@ /** | ||
*/ | ||
placeholder?: boolean; | ||
placeholder: boolean; | ||
@@ -547,3 +639,3 @@ /** | ||
*/ | ||
placeholderValue?: string; | ||
placeholderValue: string; | ||
@@ -557,3 +649,3 @@ /** | ||
*/ | ||
searchPlaceholderValue?: string; | ||
searchPlaceholderValue: string; | ||
@@ -567,3 +659,3 @@ /** | ||
*/ | ||
prependValue?: string; | ||
prependValue: string; | ||
@@ -577,3 +669,3 @@ /** | ||
*/ | ||
appendValue?: string; | ||
appendValue: string; | ||
@@ -587,3 +679,3 @@ /** | ||
*/ | ||
renderSelectedChoices?: Choices.Types.renderSelected; | ||
renderSelectedChoices: 'auto' | 'always'; | ||
@@ -597,3 +689,3 @@ /** | ||
*/ | ||
loadingText?: string; | ||
loadingText: string; | ||
@@ -607,3 +699,3 @@ /** | ||
*/ | ||
noResultsText?: string | Choices.Types.stringFunction; | ||
noResultsText: string | Choices.Types.stringFunction; | ||
@@ -617,3 +709,3 @@ /** | ||
*/ | ||
noChoicesText?: string | Choices.Types.stringFunction; | ||
noChoicesText: string | Choices.Types.stringFunction; | ||
@@ -627,17 +719,5 @@ /** | ||
*/ | ||
itemSelectText?: string; | ||
itemSelectText: string; | ||
/** | ||
* The text that is shown when a user has inputted a new item but has not pressed the enter key. To access the current input value, pass a function with a `value` argument (see the **default config** [https://github.com/jshjohnson/Choices#setup] for an example), otherwise pass a string. | ||
* | ||
* **Input types affected:** text | ||
* | ||
* @default | ||
* ``` | ||
* (value) => `Press Enter to add <b>"${value}"</b>`; | ||
* ``` | ||
*/ | ||
addItemText?: string | Choices.Types.noticeStringFunction; | ||
/** | ||
* The text that is shown when a user has focus on the input but has already reached the **max item count** [https://github.com/jshjohnson/Choices#maxitemcount]. To access the max item count, pass a function with a `maxItemCount` argument (see the **default config** [https://github.com/jshjohnson/Choices#setup] for an example), otherwise pass a string. | ||
@@ -652,3 +732,3 @@ * | ||
*/ | ||
maxItemText?: string | Choices.Types.noticeLimitFunction; | ||
maxItemText: string | Choices.Types.noticeLimitFunction; | ||
@@ -660,3 +740,3 @@ /** | ||
*/ | ||
uniqueItemText?: string | Choices.Types.noticeStringFunction; | ||
uniqueItemText: string | Choices.Types.noticeStringFunction; | ||
@@ -668,3 +748,3 @@ /** | ||
*/ | ||
classNames?: Choices.ClassNames; | ||
classNames: Partial<Choices.ClassNames>; | ||
@@ -674,9 +754,3 @@ /** | ||
*/ | ||
fuseOptions?: { | ||
[index: string]: any; | ||
/** | ||
* @default 'score' | ||
*/ | ||
include?: string; | ||
}; | ||
fuseOptions: FuseOptions<Choice>; | ||
@@ -692,3 +766,3 @@ /** | ||
*/ | ||
callbackOnInit?: () => any; | ||
callbackOnInit: (this: Choices) => void; | ||
@@ -729,49 +803,29 @@ /** | ||
*/ | ||
callbackOnCreateTemplates?: Choices.Types.callbackOnCreateTemplates; | ||
callbackOnCreateTemplates: ( | ||
template: Choices.Types.strToEl, | ||
) => Partial<Choices.Templates>; | ||
} | ||
} | ||
// Overload HTMLElement addEventListener with Choices events | ||
interface HTMLElement { | ||
addEventListener<K extends keyof Choices.EventMap>(type: K, listener: (this: HTMLElement, ev: Choices.EventMap[K]) => any, useCapture?: boolean): void; | ||
} | ||
// Exporting default class | ||
export default class Choices { | ||
idNames: any; | ||
config: Choices.Options; | ||
static readonly defaults: { | ||
readonly options: Partial<Choices.Options>; | ||
readonly templates: Choices.Templates; | ||
}; | ||
readonly config: Choices.Options; | ||
// State Tracking | ||
store: any; | ||
initialised: boolean; | ||
currentState: any; | ||
prevState: any; | ||
currentValue: string; | ||
// Element | ||
passedElement: Choices.passedElement; | ||
readonly passedElement: Choices.passedElement; | ||
// Checks | ||
isTextElement: boolean; | ||
isSelectOneElement: boolean; | ||
isSelectMultipleElement: boolean; | ||
isSelectElement: boolean; | ||
isValidElementType: boolean; | ||
isIe11: boolean; | ||
isScrollingOnIe: boolean; | ||
highlightPosition: number; | ||
canSearch: boolean; | ||
placeholder: boolean; | ||
presetChoices: Choices.Choice[]; | ||
presetItems: Choices.Item[]; | ||
constructor( | ||
selectorOrElement: string | HTMLInputElement | HTMLSelectElement, | ||
userConfig?: Partial<Choices.Options>, | ||
); | ||
readonly baseId: string; | ||
wasTap: boolean; | ||
constructor(element: string | HTMLElement | HTMLCollectionOf<Element> | NodeList, userConfig?: Choices.Options); | ||
new(element?: string | HTMLElement | HTMLCollectionOf<Element> | NodeList, userConfig?: Choices.Options): this; | ||
/** | ||
@@ -834,3 +888,2 @@ * Creates a new instance of Choices, adds event listeners, creates templates and renders a Choices element to the DOM. | ||
/** | ||
@@ -864,4 +917,31 @@ * Show option list dropdown (only affects select inputs). | ||
/** Direct populate choices | ||
* | ||
* @param {string[] | Choices.Item[]} items | ||
*/ | ||
setValue(items: string[] | Choices.Item[]): this; | ||
/** | ||
* Set choices of select input via an array of objects, a value name and a label name. | ||
* Set value of input based on existing Choice. `value` can be either a single string or an array of strings | ||
* | ||
* **Input types affected:** select-one, select-multiple | ||
* | ||
* @example | ||
* ``` | ||
* const example = new Choices(element, { | ||
* choices: [ | ||
* {value: 'One', label: 'Label One'}, | ||
* {value: 'Two', label: 'Label Two', disabled: true}, | ||
* {value: 'Three', label: 'Label Three'}, | ||
* ], | ||
* }); | ||
* | ||
* example.setChoiceByValue('Two'); // Choice with value of 'Two' has now been selected. | ||
* ``` | ||
*/ | ||
setChoiceByValue(value: string | string[]): this; | ||
/** | ||
* Set choices of select input via an array of objects (or function that returns array of object or promise of it), | ||
* a value field name and a label field name. | ||
* This behaves the same as passing items via the choices option but can be called after initialising Choices. | ||
@@ -873,4 +953,8 @@ * This can also be used to add groups of choices (see example 2); Optionally pass a true `replaceChoices` value to remove any existing choices. | ||
* | ||
* @example Example 1: | ||
* ``` | ||
* @param {string} [value = 'value'] - name of `value` field | ||
* @param {string} [label = 'label'] - name of 'label' field | ||
* @param {boolean} [replaceChoices = false] - whether to replace of add choices | ||
* | ||
* @example | ||
* ```js | ||
* const example = new Choices(element); | ||
@@ -885,4 +969,18 @@ * | ||
* | ||
* @example Example 2: | ||
* @example | ||
* ```js | ||
* const example = new Choices(element); | ||
* | ||
* example.setChoices(async () => { | ||
* try { | ||
* const items = await fetch('/items'); | ||
* return items.json() | ||
* } catch(err) { | ||
* console.error(err) | ||
* } | ||
* }); | ||
* ``` | ||
* | ||
* @example | ||
* ```js | ||
* const example = new Choices(element); | ||
@@ -915,27 +1013,18 @@ * | ||
*/ | ||
setValue(args: string[]): this; | ||
setChoices< | ||
T extends object[] | ((instance: Choices) => object[] | Promise<object[]>) | ||
>( | ||
choices: T, | ||
value?: string, | ||
label?: string, | ||
replaceChoices?: boolean, | ||
): T extends object[] ? this : Promise<this>; | ||
/** | ||
* Set value of input based on existing Choice. `value` can be either a single string or an array of strings | ||
* Clear all choices from select. | ||
* | ||
* **Input types affected:** select-one, select-multiple | ||
* | ||
* @example | ||
* ``` | ||
* const example = new Choices(element, { | ||
* choices: [ | ||
* {value: 'One', label: 'Label One'}, | ||
* {value: 'Two', label: 'Label Two', disabled: true}, | ||
* {value: 'Three', label: 'Label Three'}, | ||
* ], | ||
* }); | ||
* | ||
* example.setChoiceByValue('Two'); // Choice with value of 'Two' has now been selected. | ||
* ``` | ||
*/ | ||
setChoiceByValue(value: string | string[]): this; | ||
clearChoices(): this; | ||
/** Direct populate choices */ | ||
setChoices(choices: Choices.Choice[], value: string, label: string, replaceChoices?: boolean): this; | ||
/** | ||
@@ -968,38 +1057,2 @@ * Removes all items, choices and groups. Use with caution. | ||
disable(): this; | ||
/** | ||
* Populate choices/groups via a callback. | ||
* | ||
* **Input types affected:** select-one, select-multiple | ||
* | ||
* @example | ||
* ``` | ||
* var example = new Choices(element); | ||
* | ||
* example.ajax(function(callback) { | ||
* fetch(url) | ||
* .then(function(response) { | ||
* response.json().then(function(data) { | ||
* callback(data, 'value', 'label'); | ||
* }); | ||
* }) | ||
* .catch(function(error) { | ||
* console.log(error); | ||
* }); | ||
* }); | ||
* ``` | ||
*/ | ||
ajax(fn: (values: any) => any): this; | ||
/** Render group choices into a DOM fragment and append to choice list */ | ||
private createGroupsFragment(groups: Choices.Group[], choices: Choices.Choice[], fragment: DocumentFragment): DocumentFragment; | ||
/** Render choices into a DOM fragment and append to choice list */ | ||
private createChoicesFragment(choices: Choices.Choice[], fragment: DocumentFragment, withinGroup?: boolean): DocumentFragment; | ||
/** Render items into a DOM fragment and append to items list */ | ||
private _createItemsFragment(items: Choices.Item[], fragment?: DocumentFragment): void; | ||
/** Render DOM with values */ | ||
private render(): void; | ||
} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
3
39
1086
442403
34
9145
+ Addedfuse.js@3.6.1(transitive)
- Removedclassnames@^2.2.6
- Removedclassnames@2.5.1(transitive)
- Removedfuse.js@3.4.2(transitive)
Updatedfuse.js@^3.4.5