Code refactoring (#735)

* Add placeholder options to demo page

* Use constant types in components

* Refactor adding predefined groups/items/choices

* Add 'highlighted' flag to Item type

* Fix dispatch param type

* Build

* Add jsdoc comments to utils

* Remove unused file

* Add default values to js doc comments

* Use Redux Action type

* Housekeeping

* Increase utils coverage

* Apply suggestions from code review

* Add _getTemplate unit tests
This commit is contained in:
Josh Johnson 2019-11-03 17:45:16 +00:00 committed by GitHub
parent e6882f3e4b
commit ab22347d7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1198 additions and 807 deletions

66
package-lock.json generated
View file

@ -1944,7 +1944,7 @@
},
"browserify-aes": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
"integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
"dev": true,
"requires": {
@ -1981,7 +1981,7 @@
},
"browserify-rsa": {
"version": "4.0.1",
"resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
"resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
"integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
"dev": true,
"requires": {
@ -2026,7 +2026,7 @@
},
"buffer": {
"version": "4.9.1",
"resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
"integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
"dev": true,
"requires": {
@ -2213,7 +2213,7 @@
},
"camelcase-keys": {
"version": "2.1.0",
"resolved": "http://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
"integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
"dev": true,
"requires": {
@ -2807,7 +2807,7 @@
},
"create-hash": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
"dev": true,
"requires": {
@ -2820,7 +2820,7 @@
},
"create-hmac": {
"version": "1.1.7",
"resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
"resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
"integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
"dev": true,
"requires": {
@ -3350,7 +3350,7 @@
},
"diffie-hellman": {
"version": "5.0.3",
"resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
"resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
"integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
"dev": true,
"requires": {
@ -3420,7 +3420,7 @@
},
"duplexer": {
"version": "0.1.1",
"resolved": "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
"integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=",
"dev": true
},
@ -4092,7 +4092,7 @@
"dependencies": {
"pify": {
"version": "2.3.0",
"resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true
}
@ -5873,7 +5873,7 @@
},
"get-stream": {
"version": "3.0.0",
"resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
"dev": true
},
@ -6038,7 +6038,7 @@
},
"got": {
"version": "6.7.1",
"resolved": "http://registry.npmjs.org/got/-/got-6.7.1.tgz",
"resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz",
"integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=",
"dev": true,
"requires": {
@ -6903,7 +6903,7 @@
},
"is-obj": {
"version": "1.0.1",
"resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
"dev": true
},
@ -7866,7 +7866,7 @@
},
"load-json-file": {
"version": "2.0.0",
"resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
"integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=",
"dev": true,
"requires": {
@ -7887,7 +7887,7 @@
},
"pify": {
"version": "2.3.0",
"resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true
}
@ -8145,7 +8145,7 @@
},
"media-typer": {
"version": "0.3.0",
"resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
"dev": true
},
@ -8186,7 +8186,7 @@
},
"meow": {
"version": "3.7.0",
"resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
"resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
"integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
"dev": true,
"requires": {
@ -8214,7 +8214,7 @@
},
"load-json-file": {
"version": "1.1.0",
"resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
"integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
"dev": true,
"requires": {
@ -8256,7 +8256,7 @@
},
"pify": {
"version": "2.3.0",
"resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true
},
@ -8454,7 +8454,7 @@
},
"mkdirp": {
"version": "0.5.1",
"resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
@ -8724,7 +8724,7 @@
"dependencies": {
"semver": {
"version": "5.3.0",
"resolved": "http://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
"integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
"dev": true
}
@ -9603,7 +9603,7 @@
},
"onetime": {
"version": "1.1.0",
"resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
"integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
"dev": true
},
@ -9653,7 +9653,7 @@
},
"ora": {
"version": "0.2.3",
"resolved": "http://registry.npmjs.org/ora/-/ora-0.2.3.tgz",
"resolved": "https://registry.npmjs.org/ora/-/ora-0.2.3.tgz",
"integrity": "sha1-N1J9Igrc1Tw5tzVx11QVbV22V6Q=",
"dev": true,
"requires": {
@ -9704,7 +9704,7 @@
},
"os-locale": {
"version": "1.4.0",
"resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
"resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
"integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
"dev": true,
"requires": {
@ -9940,7 +9940,7 @@
"dependencies": {
"pify": {
"version": "2.3.0",
"resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true
}
@ -11770,7 +11770,7 @@
},
"safe-regex": {
"version": "1.1.0",
"resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
"integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
"dev": true,
"requires": {
@ -12134,7 +12134,7 @@
},
"sha.js": {
"version": "2.4.11",
"resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
"dev": true,
"requires": {
@ -12201,6 +12201,12 @@
"supports-color": "^5.5.0"
}
},
"sinon-chai": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.3.0.tgz",
"integrity": "sha512-r2JhDY7gbbmh5z3Q62pNbrjxZdOAjpsqW/8yxAZRSqLZqowmfGZPGUZPFf3UX36NLis0cv8VEM5IJh9HgkSOAA==",
"dev": true
},
"slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@ -12209,7 +12215,7 @@
},
"slice-ansi": {
"version": "0.0.4",
"resolved": "http://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz",
"integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=",
"dev": true
},
@ -12629,7 +12635,7 @@
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true,
"requires": {
@ -12644,7 +12650,7 @@
},
"strip-eof": {
"version": "1.0.0",
"resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
"dev": true
},
@ -12977,7 +12983,7 @@
},
"through": {
"version": "2.3.8",
"resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
"dev": true
},

View file

@ -84,6 +84,7 @@
"postcss-cli": "^6.1.3",
"prettier": "^1.18.2",
"sinon": "^7.5.0",
"sinon-chai": "^3.3.0",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.9",
"webpack-dev-middleware": "^3.7.2",

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -214,6 +214,7 @@
placeholder="This is a placeholder"
multiple
>
<option value="">Choose a city</option>
<optgroup label="UK">
<option value="London">London</option>
<option value="Manchester">Manchester</option>
@ -336,8 +337,8 @@
data-trigger
name="choices-single-groups"
id="choices-single-groups"
placeholder="This is a placeholder"
>
<option value="">Choose a city</option>
<optgroup label="UK">
<option value="London">London</option>
<option value="Manchester">Manchester</option>
@ -376,7 +377,6 @@
data-trigger
name="choices-single-rtl"
id="choices-single-rtl"
placeholder="This is a placeholder"
dir="rtl"
>
<option value="Choice 1">Choice 1</option>
@ -402,7 +402,6 @@
class="form-control"
name="choices-single-preset-options"
id="choices-single-preset-options"
placeholder="This is a placeholder"
></select>
<label for="choices-single-selected-option"
@ -415,7 +414,6 @@
class="form-control"
name="choices-single-selected-option"
id="choices-single-selected-option"
placeholder="This is a placeholder"
></select>
<label for="choices-with-custom-props-via-html"
@ -440,7 +438,6 @@
class="form-control"
name="choices-single-no-sorting"
id="choices-single-no-sorting"
placeholder="This is a placeholder"
>
<option value="Madrid">Madrid</option>
<option value="Toronto">Toronto</option>
@ -467,7 +464,6 @@
class="form-control"
name="choices-single-custom-templates"
id="choices-single-custom-templates"
placeholder="This is a placeholder"
>
<option value="React">React</option>
<option value="Angular">Angular</option>
@ -481,12 +477,8 @@
'Cities' is 'London'
</p>
<label for="cities">Cities</label>
<select
class="form-control"
name="cities"
id="cities"
placeholder="Choose a city"
>
<select class="form-control" name="cities" id="cities">
<option value="">Choose a city</option>
<option value="Leeds">Leeds</option>
<option value="Manchester">Manchester</option>
<option value="London">London</option>
@ -495,12 +487,8 @@
</select>
<label for="tube-stations">Tube stations</label>
<select
class="form-control"
name="tube-stations"
id="tube-stations"
placeholder="Choose a tube station"
>
<select class="form-control" name="tube-stations" id="tube-stations">
<option value="">Choose a tube station</option>
<option value="Moorgate">Moorgate</option>
<option value="St Pauls">St Pauls</option>
<option value="Old Street">Old Street</option>
@ -515,12 +503,7 @@
<p>Change the values and press reset to restore to initial state.</p>
<form>
<label for="reset-simple">Change me!</label>
<select
class="form-control"
name="reset-simple"
id="reset-simple"
placeholder="Choose an option"
>
<select class="form-control" name="reset-simple" id="reset-simple">
<option value="Option 1">Option 1</option>
<option value="Option 2" selected>Option 2</option>
<option value="Option 3">Option 3</option>
@ -533,7 +516,6 @@
class="form-control"
name="reset-multiple"
id="reset-multiple"
placeholder="This is a placeholder"
multiple
>
<option value="Choice 1" selected>Choice 1</option>

View file

@ -1,4 +1,5 @@
/**
* @typedef {import('redux').Action} Action
* @typedef {import('../../../types/index').Choices.Choice} Choice
*/
@ -6,7 +7,7 @@ import { ACTION_TYPES } from '../constants';
/**
* @argument {Choice} choice
* @returns {{ type: string } & Choice}
* @returns {Action & Choice}
*/
export const addChoice = ({
value,
@ -33,7 +34,7 @@ export const addChoice = ({
/**
* @argument {Choice[]} results
* @returns {{ type: string, results: Choice[] }}
* @returns {Action & { results: Choice[] }}
*/
export const filterChoices = results => ({
type: ACTION_TYPES.FILTER_CHOICES,
@ -42,7 +43,7 @@ export const filterChoices = results => ({
/**
* @argument {boolean} active
* @returns {{ type: string, active: boolean }}
* @returns {Action & { active: boolean }}
*/
export const activateChoices = (active = true) => ({
type: ACTION_TYPES.ACTIVATE_CHOICES,
@ -50,7 +51,7 @@ export const activateChoices = (active = true) => ({
});
/**
* @returns {{ type: string }}
* @returns {Action}
*/
export const clearChoices = () => ({
type: ACTION_TYPES.CLEAR_CHOICES,

View file

@ -1,12 +1,13 @@
import { ACTION_TYPES } from '../constants';
/**
* @typedef {import('redux').Action} Action
* @typedef {import('../../../types/index').Choices.Group} Group
*/
/**
* @param {Group} group
* @returns {{ type: string } & Group}
* @returns {Action & Group}
*/
export const addGroup = ({ value, id, active, disabled }) => ({
type: ACTION_TYPES.ADD_GROUP,

View file

@ -1,12 +1,13 @@
import { ACTION_TYPES } from '../constants';
/**
* @typedef {import('redux').Action} Action
* @typedef {import('../../../types/index').Choices.Item} Item
*/
/**
* @param {Item} item
* @returns {{ type: string } & Item}
* @returns {Action & Item}
*/
export const addItem = ({
value,
@ -32,7 +33,7 @@ export const addItem = ({
/**
* @param {string} id
* @param {string} choiceId
* @returns {{ type: string, id: string, choiceId: string }}
* @returns {Action & { id: string, choiceId: string }}
*/
export const removeItem = (id, choiceId) => ({
type: ACTION_TYPES.REMOVE_ITEM,
@ -43,7 +44,7 @@ export const removeItem = (id, choiceId) => ({
/**
* @param {string} id
* @param {boolean} highlighted
* @returns {{ type: string, id: string, highlighted: boolean }}
* @returns {Action & { id: string, highlighted: boolean }}
*/
export const highlightItem = (id, highlighted) => ({
type: ACTION_TYPES.HIGHLIGHT_ITEM,

View file

@ -1,5 +1,9 @@
/**
* @returns {{ type: string }}
* @typedef {import('redux').Action} Action
*/
/**
* @returns {Action}
*/
export const clearAll = () => ({
type: 'CLEAR_ALL',
@ -7,7 +11,7 @@ export const clearAll = () => ({
/**
* @param {any} state
* @returns {{ type: string, state: object }}
* @returns {Action & { state: object }}
*/
export const resetTo = state => ({
type: 'RESET_TO',
@ -16,7 +20,7 @@ export const resetTo = state => ({
/**
* @param {boolean} isLoading
* @returns {{ type: string, isLoading: boolean }}
* @returns {Action & { isLoading: boolean }}
*/
export const setIsLoading = isLoading => ({
type: 'SET_IS_LOADING',

View file

@ -45,6 +45,8 @@ import {
/**
* @typedef {import('../../types/index').Choices.Choice} Choice
* @typedef {import('../../types/index').Choices.Item} Item
* @typedef {import('../../types/index').Choices.Group} Group
* @typedef {import('../../types/index').Choices.Options} Options
*/
@ -174,16 +176,31 @@ class Choices {
this._idNames = {
itemChoice: 'item-choice',
};
// Assign preset groups from passed element
this._presetGroups = this.passedElement.optionGroups;
// Assign preset choices from passed object
this._presetChoices = this.config.choices;
// Assign preset items from passed object first
this._presetItems = this.config.items;
// Then add any values passed from attribute
// Add any values passed from attribute
if (this.passedElement.value) {
this._presetItems = this._presetItems.concat(
this.passedElement.value.split(this.config.delimiter),
);
}
// Create array of choices from option elements
if (this.passedElement.options) {
this.passedElement.options.forEach(o => {
this._presetChoices.push({
value: o.value,
label: o.innerHTML,
selected: o.selected,
disabled: o.disabled || o.parentNode.disabled,
placeholder: o.value === '' || o.hasAttribute('placeholder'),
customProperties: o.getAttribute('data-custom-properties'),
});
});
}
this._render = this._render.bind(this);
this._onFocus = this._onFocus.bind(this);
@ -466,7 +483,7 @@ class Choices {
*
* **Input types affected:** select-one, select-multiple
*
* @template {object[] | ((instance: Choices) => object[] | Promise<object[]>)} T
* @template {Choice[] | ((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
@ -556,6 +573,7 @@ class Choices {
if (typeof choicesArrayOrFetcher === 'function') {
// it's a choices fetcher function
const fetcher = choicesArrayOrFetcher(this);
if (typeof Promise === 'function' && fetcher instanceof Promise) {
// that's a promise
// eslint-disable-next-line compat/compat
@ -591,7 +609,9 @@ class Choices {
this.containerOuter.removeLoadingState();
const addGroupsAndChoices = groupOrChoice => {
this._setLoading(true);
choicesArrayOrFetcher.forEach(groupOrChoice => {
if (groupOrChoice.choices) {
this._addGroup({
group: groupOrChoice,
@ -609,10 +629,8 @@ class Choices {
placeholder: groupOrChoice.placeholder,
});
}
};
});
this._setLoading(true);
choicesArrayOrFetcher.forEach(addGroupsAndChoices);
this._setLoading(false);
return this;
@ -2024,7 +2042,7 @@ class Choices {
this.input.placeholder = this.config.searchPlaceholderValue || '';
} else if (this._placeholderValue) {
this.input.placeholder = this._placeholderValue;
this.input.setWidth(true);
this.input.setWidth();
}
this.containerOuter.element.appendChild(this.containerInner.element);
@ -2045,116 +2063,105 @@ class Choices {
}
if (this._isSelectElement) {
this._addPredefinedChoices();
} else if (this._isTextElement) {
this._addPredefinedItems();
this._highlightPosition = 0;
this._isSearching = false;
this._setLoading(true);
if (this._presetGroups.length) {
this._addPredefinedGroups(this._presetGroups);
} else {
this._addPredefinedChoices(this._presetChoices);
}
this._setLoading(false);
}
if (this._isTextElement) {
this._addPredefinedItems(this._presetItems);
}
}
_addPredefinedChoices() {
const passedGroups = this.passedElement.optionGroups;
this._highlightPosition = 0;
this._isSearching = false;
this._setLoading(true);
if (passedGroups && passedGroups.length) {
// If we have a placeholder option
const placeholderChoice = this.passedElement.placeholderOption;
if (
placeholderChoice &&
placeholderChoice.parentNode.tagName === 'SELECT'
) {
this._addChoice({
value: placeholderChoice.value,
label: placeholderChoice.innerHTML,
isSelected: placeholderChoice.selected,
isDisabled: placeholderChoice.disabled,
placeholder: true,
});
}
passedGroups.forEach(group =>
this._addGroup({
group,
id: group.id || null,
}),
);
} else {
const passedOptions = this.passedElement.options;
const filter = this.config.sorter;
const allChoices = this._presetChoices;
// Create array of options from option elements
passedOptions.forEach(o => {
allChoices.push({
value: o.value,
label: o.innerHTML,
selected: o.selected,
disabled: o.disabled || o.parentNode.disabled,
placeholder: o.value === '' || o.hasAttribute('placeholder'),
customProperties: o.getAttribute('data-custom-properties'),
});
_addPredefinedGroups(groups) {
// If we have a placeholder option
const placeholderChoice = this.passedElement.placeholderOption;
if (
placeholderChoice &&
placeholderChoice.parentNode.tagName === 'SELECT'
) {
this._addChoice({
value: placeholderChoice.value,
label: placeholderChoice.innerHTML,
isSelected: placeholderChoice.selected,
isDisabled: placeholderChoice.disabled,
placeholder: true,
});
}
// If sorting is enabled or the user is searching, filter choices
if (this.config.shouldSort) {
allChoices.sort(filter);
}
groups.forEach(group =>
this._addGroup({
group,
id: group.id || null,
}),
);
}
// Determine whether there is a selected choice
const hasSelectedChoice = allChoices.some(choice => choice.selected);
const handleChoice = (choice, index) => {
const { value, label, customProperties, placeholder } = choice;
_addPredefinedChoices(choices) {
// If sorting is enabled or the user is searching, filter choices
if (this.config.shouldSort) {
choices.sort(this.config.sorter);
}
if (this._isSelectElement) {
// If the choice is actually a group
if (choice.choices) {
this._addGroup({
group: choice,
id: choice.id || null,
});
} else {
// If there is a selected choice already or the choice is not
// the first in the array, add each choice normally
// Otherwise pre-select the first choice in the array if it's a single select
const shouldPreselect =
this._isSelectOneElement && !hasSelectedChoice && index === 0;
const isSelected = shouldPreselect ? true : choice.selected;
const isDisabled = shouldPreselect ? false : choice.disabled;
// Determine whether there is a selected choice
const hasSelectedChoice = choices.some(choice => choice.selected);
this._addChoice({
value,
label,
isSelected,
isDisabled,
customProperties,
placeholder,
});
}
// Add each choice
choices.forEach((choice, index) => {
const { value, label, customProperties, placeholder } = choice;
if (this._isSelectElement) {
// If the choice is actually a group
if (choice.choices) {
this._addGroup({
group: choice,
id: choice.id || null,
});
} else {
// If there is a selected choice already or the choice is not
// the first in the array, add each choice normally
// Otherwise pre-select the first choice in the array if it's a single select
const shouldPreselect =
this._isSelectOneElement && !hasSelectedChoice && index === 0;
const isSelected = shouldPreselect ? true : choice.selected;
const isDisabled = shouldPreselect ? false : choice.disabled;
this._addChoice({
value,
label,
isSelected: choice.selected,
isDisabled: choice.disabled,
isSelected,
isDisabled,
customProperties,
placeholder,
});
}
};
// Add each choice
allChoices.forEach((choice, index) => handleChoice(choice, index));
}
this._setLoading(false);
} else {
this._addChoice({
value,
label,
isSelected: choice.selected,
isDisabled: choice.disabled,
customProperties,
placeholder,
});
}
});
}
_addPredefinedItems() {
const handlePresetItem = item => {
const itemType = getType(item);
if (itemType === 'Object' && item.value) {
/**
* @param {Item[]} items
*/
_addPredefinedItems(items) {
items.forEach(item => {
if (typeof item === 'object' && item.value) {
this._addItem({
value: item.value,
label: item.label,
@ -2162,14 +2169,14 @@ class Choices {
customProperties: item.customProperties,
placeholder: item.placeholder,
});
} else if (itemType === 'String') {
}
if (typeof item === 'string') {
this._addItem({
value: item,
});
}
};
this._presetItems.forEach(item => handlePresetItem(item));
});
}
_setChoiceOrItem(item) {

View file

@ -1,10 +1,13 @@
import { expect } from 'chai';
import chai, { expect } from 'chai';
import { spy, stub } from 'sinon';
import sinonChai from 'sinon-chai';
import Choices from './choices';
import { EVENTS, ACTION_TYPES, DEFAULT_CONFIG } from './constants';
import { WrappedSelect, WrappedInput } from './components/index';
chai.use(sinonChai);
describe('choices', () => {
let instance;
let output;
@ -1964,5 +1967,33 @@ describe('choices', () => {
});
});
});
describe('_getTemplate', () => {
describe('when not passing a template key', () => {
it('returns null', () => {
output = instance._getTemplate();
expect(output).to.equal(null);
});
});
describe('when passing a template key', () => {
it('returns the generated template for the given template key', () => {
const templateKey = 'test';
const element = document.createElement('div');
const customArg = { test: true };
instance._templates = {
[templateKey]: stub().returns(element),
};
output = instance._getTemplate(templateKey, customArg);
expect(output).to.deep.equal(element);
expect(instance._templates[templateKey]).to.have.been.calledOnceWith(
instance.config.classNames,
customArg,
);
});
});
});
});
});

View file

@ -1,4 +1,5 @@
import { wrap } from '../lib/utils';
import { SELECT_ONE_TYPE } from '../constants';
/**
* @typedef {import('../../../types/index').Choices.passedElement} passedElement
@ -116,7 +117,7 @@ export default class Container {
enable() {
this.element.classList.remove(this.classNames.disabledState);
this.element.removeAttribute('aria-disabled');
if (this.type === 'select-one') {
if (this.type === SELECT_ONE_TYPE) {
this.element.setAttribute('tabindex', '0');
}
this.isDisabled = false;
@ -125,7 +126,7 @@ export default class Container {
disable() {
this.element.classList.add(this.classNames.disabledState);
this.element.setAttribute('aria-disabled', 'true');
if (this.type === 'select-one') {
if (this.type === SELECT_ONE_TYPE) {
this.element.setAttribute('tabindex', '-1');
}
this.isDisabled = true;

View file

@ -1,4 +1,5 @@
import { sanitise } from '../lib/utils';
import { SELECT_ONE_TYPE } from '../constants';
/**
* @typedef {import('../../../types/index').Choices.passedElement} passedElement
@ -137,7 +138,7 @@ export default class Input {
}
_onInput() {
if (this.type !== 'select-one') {
if (this.type !== SELECT_ONE_TYPE) {
this.setWidth();
}
}

View file

@ -18,7 +18,7 @@ export default class List {
}
/**
* @param {Element} node
* @param {Element | DocumentFragment} node
*/
append(node) {
this.element.appendChild(node);

View file

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

View file

@ -1,9 +1,23 @@
/**
* @param {number} min
* @param {number} max
* @returns {number}
*/
export const getRandomNumber = (min, max) =>
Math.floor(Math.random() * (max - min) + min);
/**
* @param {number} length
* @returns {string}
*/
export const generateChars = length =>
Array.from({ length }, () => getRandomNumber(0, 36).toString(36)).join('');
/**
* @param {HTMLInputElement | HTMLSelectElement} element
* @param {string} prefix
* @returns {string}
*/
export const generateId = (element, prefix) => {
let id =
element.id ||
@ -15,11 +29,25 @@ export const generateId = (element, prefix) => {
return id;
};
/**
* @param {any} obj
* @returns {string}
*/
export const getType = obj => Object.prototype.toString.call(obj).slice(8, -1);
/**
* @param {string} type
* @param {any} obj
* @returns {boolean}
*/
export const isType = (type, obj) =>
obj !== undefined && obj !== null && getType(obj) === type;
/**
* @param {HTMLElement} element
* @param {HTMLElement} [wrapper={HTMLDivElement}]
* @returns {HTMLElement}
*/
export const wrap = (element, wrapper = document.createElement('div')) => {
if (element.nextSibling) {
element.parentNode.insertBefore(wrapper, element.nextSibling);
@ -36,34 +64,39 @@ export const wrap = (element, wrapper = document.createElement('div')) => {
*/
export const findAncestorByAttrName = (el, attr) => el.closest(`[${attr}]`);
export const getAdjacentEl =
/**
* @param {Element} startEl
* @param {string} selector
* @param {1 | -1} direction
* @returns {Element | undefined}
*/
(startEl, selector, direction = 1) => {
if (!(startEl instanceof Element) || typeof selector !== 'string') {
return undefined;
/**
* @param {Element} startEl
* @param {string} selector
* @param {1 | -1} direction
* @returns {Element | undefined}
*/
export const getAdjacentEl = (startEl, selector, direction = 1) => {
if (!(startEl instanceof Element) || typeof selector !== 'string') {
return undefined;
}
const prop = `${direction > 0 ? 'next' : 'previous'}ElementSibling`;
let sibling = startEl[prop];
while (sibling) {
if (sibling.matches(selector)) {
return sibling;
}
sibling = sibling[prop];
}
const prop = `${direction > 0 ? 'next' : 'previous'}ElementSibling`;
return sibling;
};
let sibling = startEl[prop];
while (sibling) {
if (sibling.matches(selector)) {
return sibling;
}
sibling = sibling[prop];
}
return sibling;
};
export const isScrolledIntoView = (el, parent, direction = 1) => {
if (!el) {
return;
/**
* @param {HTMLElement} element
* @param {HTMLElement} parent
* @param {-1 | 1} direction
* @returns {boolean}
*/
export const isScrolledIntoView = (element, parent, direction = 1) => {
if (!element) {
return false;
}
let isVisible;
@ -71,15 +104,20 @@ export const isScrolledIntoView = (el, parent, direction = 1) => {
if (direction > 0) {
// In view from bottom
isVisible =
parent.scrollTop + parent.offsetHeight >= el.offsetTop + el.offsetHeight;
parent.scrollTop + parent.offsetHeight >=
element.offsetTop + element.offsetHeight;
} else {
// In view from top
isVisible = el.offsetTop >= parent.scrollTop;
isVisible = element.offsetTop >= parent.scrollTop;
}
return isVisible;
};
/**
* @param {any} value
* @returns {any}
*/
export const sanitise = value => {
if (typeof value !== 'string') {
return value;
@ -92,6 +130,9 @@ export const sanitise = value => {
.replace(/"/g, '&quot;');
};
/**
* @returns {function}
*/
export const strToEl = (() => {
const tmpEl = document.createElement('div');
@ -124,11 +165,16 @@ export const sortByAlpha = (
});
/**
* @param {object} a
* @param {object} b
* @param {{ score: number }} a
* @param {{ score: number }} b
*/
export const sortByScore = (a, b) => a.score - b.score;
/**
* @param {HTMLElement} element
* @param {string} type
* @param {object} customArgs
*/
export const dispatchEvent = (element, type, customArgs = null) => {
const event = new CustomEvent(type, {
detail: customArgs,
@ -139,9 +185,19 @@ export const dispatchEvent = (element, type, customArgs = null) => {
return element.dispatchEvent(event);
};
/**
* @param {string} userAgent
* @returns {boolean}
*/
export const isIE11 = userAgent =>
!!(userAgent.match(/Trident/) && userAgent.match(/rv[ :]11/));
/**
* @param {array} array
* @param {any} value
* @param {string} [key="value"]
* @returns {boolean}
*/
export const existsInArray = (array, value, key = 'value') =>
array.some(item => {
if (typeof value === 'string') {
@ -151,8 +207,18 @@ export const existsInArray = (array, value, key = 'value') =>
return item[key] === value;
});
/**
* @param {any} obj
* @returns {any}
*/
export const cloneObject = obj => JSON.parse(JSON.stringify(obj));
/**
* Returns an array of keys present on the first but missing on the second object
* @param {object} a
* @param {object} b
* @returns {string[]}
*/
export const diff = (a, b) => {
const aKeys = Object.keys(a).sort();
const bKeys = Object.keys(b).sort();

View file

@ -13,6 +13,7 @@ import {
existsInArray,
cloneObject,
dispatchEvent,
diff,
} from './utils';
describe('utils', () => {
@ -37,7 +38,7 @@ describe('utils', () => {
describe('generateId', () => {
describe('when given element has id value', () => {
it('generates a unique prefixed id based on given elements id', () => {
const element = document.createElement('div');
const element = document.createElement('select');
element.id = 'test-id';
const prefix = 'test-prefix';
@ -49,7 +50,7 @@ describe('utils', () => {
describe('when given element has no id value but name value', () => {
it('generates a unique prefixed id based on given elements name plus 2 random characters', () => {
const element = document.createElement('div');
const element = document.createElement('select');
element.name = 'test-name';
const prefix = 'test-prefix';
@ -63,7 +64,7 @@ describe('utils', () => {
describe('when given element has no id value and no name value', () => {
it('generates a unique prefixed id based on 4 random characters', () => {
const element = document.createElement('div');
const element = document.createElement('select');
const prefix = 'test-prefix';
const output = generateId(element, prefix);
@ -83,7 +84,7 @@ describe('utils', () => {
expect(getType([])).to.equal('Array');
expect(getType(() => {})).to.equal('Function');
expect(getType(new Error())).to.equal('Error');
expect(getType(new RegExp())).to.equal('RegExp');
expect(getType(new RegExp(/''/g))).to.equal('RegExp');
expect(getType(new String())).to.equal('String'); // eslint-disable-line
expect(getType('')).to.equal('String');
});
@ -97,12 +98,24 @@ describe('utils', () => {
});
describe('sanitise', () => {
it('strips HTML from value', () => {
const value = '<script>somethingMalicious();</script>';
const output = sanitise(value);
expect(output).to.equal(
'&lt;script&rt;somethingMalicious();&lt;/script&rt;',
);
describe('when passing a parameter that is not a string', () => {
it('returns the passed argument', () => {
const value = {
test: true,
};
const output = sanitise(value);
expect(output).to.equal(value);
});
});
describe('when passing a string', () => {
it('strips HTML from value', () => {
const value = '<script>somethingMalicious();</script>';
const output = sanitise(value);
expect(output).to.equal(
'&lt;script&rt;somethingMalicious();&lt;/script&rt;',
);
});
});
});
@ -238,4 +251,20 @@ describe('utils', () => {
expect(output).to.eql(object);
});
});
describe('diff', () => {
it('returns an array of keys present on the first but missing on the second object', () => {
const obj1 = {
foo: 'bar',
baz: 'foo',
};
const obj2 = {
foo: 'bar',
};
const output = diff(obj1, obj2);
expect(output).to.deep.equal(['baz']);
});
});
});

View file

@ -27,7 +27,7 @@ export default class Store {
/**
* Dispatch event to store (wrapped Redux method)
* @param {Function} action Action function to trigger
* @param {{ type: string, [x: string]: any }} action Action to trigger
* @return
*/
dispatch(action) {

5
types/index.d.ts vendored
View file

@ -1,4 +1,4 @@
// Type definitions for Choices.js 7.1.x
// Type definitions for Choices.js
// Project: https://github.com/jshjohnson/Choices
// Definitions by:
// Arthur vasconcelos <https://github.com/arthurvasconcelos>,
@ -46,6 +46,7 @@ declare namespace Choices {
interface Item extends Choice {
choiceId?: string;
keyCode?: number;
highlighted?: boolean;
}
/**
@ -64,7 +65,7 @@ declare namespace Choices {
value: string;
label: string;
groupValue: string;
keyCode: string;
keyCode: number;
}>;
/**