mirror of
https://github.com/Choices-js/Choices.git
synced 2024-04-26 11:20:33 +02:00
Fix constructor (#693)
* breaking test
* Remove ablity to pass multiple elements + tests
* Update readme
* Update README.md
* 🔖 Version 8.0.0
* Remove type definition hack
* Update coverage command
* Add some missing list tests
* Remove .only
* Update demo page to loop over elements
* Update constructor to set initialised flag if already active
* Make templates private
* Throw type error once if element is invalid
* Fix list children bug
* Re-add generic examples to index.html
* Housekeeping
* Use typeof instead of isType where applicable
* Remove isElement
* Add test for isIE11
This commit is contained in:
parent
88f63faa0b
commit
0e44a916e3
|
@ -64,7 +64,8 @@
|
|||
},
|
||||
"rules": {
|
||||
"no-restricted-syntax": "off",
|
||||
"compat/compat": "off"
|
||||
"compat/compat": "off",
|
||||
"no-new": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
4
.github/workflows/publish.yml
vendored
4
.github/workflows/publish.yml
vendored
|
@ -20,7 +20,7 @@ jobs:
|
|||
npm ci
|
||||
npm run build
|
||||
npx bundlesize
|
||||
npm run coverage
|
||||
npm run test:unit:coverage
|
||||
npm run test:e2e
|
||||
env:
|
||||
CI: true
|
||||
|
@ -85,4 +85,4 @@ jobs:
|
|||
env:
|
||||
ACTIONS_DEPLOY_KEY: ${{ secrets.ACTIONS_DEPLOY_KEY }}
|
||||
PUBLISH_BRANCH: gh-pages
|
||||
PUBLISH_DIR: ./public
|
||||
PUBLISH_DIR: ./public
|
||||
|
|
2
.github/workflows/unit-tests.yml
vendored
2
.github/workflows/unit-tests.yml
vendored
|
@ -25,7 +25,7 @@ jobs:
|
|||
CYPRESS_INSTALL_BINARY: 0
|
||||
HUSKY_SKIP_INSTALL: true
|
||||
|
||||
- run: npm run coverage
|
||||
- run: npm run test:unit:coverage
|
||||
env:
|
||||
FORCE_COLOR: 2
|
||||
|
||||
|
|
13
README.md
13
README.md
|
@ -67,14 +67,11 @@ Or include Choices directly:
|
|||
|
||||
## 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);
|
||||
|
||||
// Pass reference
|
||||
|
@ -84,8 +81,8 @@ will be returned. If you target one element, that instance will be returned.
|
|||
// Pass jQuery element
|
||||
const choices = new Choices($('.js-choice')[0]);
|
||||
|
||||
// Passing options (with default options)
|
||||
const choices = new Choices(elements, {
|
||||
// Passing options (with default options)
|
||||
const choices = new Choices(element, {
|
||||
silent: false,
|
||||
items: [],
|
||||
choices: [],
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
|
||||
const { JSDOM } = require('jsdom');
|
||||
|
||||
const jsdom = new JSDOM(
|
||||
|
@ -36,6 +38,8 @@ global.HTMLElement = window.HTMLElement;
|
|||
global.Option = window.Option;
|
||||
global.HTMLOptionElement = window.HTMLOptionElement;
|
||||
global.HTMLOptGroupElement = window.HTMLOptGroupElement;
|
||||
global.HTMLSelectElement = window.HTMLSelectElement;
|
||||
global.HTMLInputElement = window.HTMLInputElement;
|
||||
global.DocumentFragment = window.DocumentFragment;
|
||||
global.requestAnimationFrame = window.requestAnimationFrame;
|
||||
|
||||
|
|
2
package-lock.json
generated
2
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "choices.js",
|
||||
"version": "7.1.5",
|
||||
"version": "8.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"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.js",
|
||||
"types": "./types/index.d.ts",
|
||||
|
@ -8,7 +8,6 @@
|
|||
"start": "run-p js:watch css:watch",
|
||||
"build": "run-p js:build css:build",
|
||||
"lint": "eslint src/scripts",
|
||||
"coverage": "NODE_ENV=test nyc --reporter=lcov --reporter=text --reporter=text-summary mocha",
|
||||
"bundlesize": "bundlesize",
|
||||
"cypress:run": "$(npm bin)/cypress run",
|
||||
"cypress:open": "$(npm bin)/cypress open",
|
||||
|
@ -16,6 +15,7 @@
|
|||
"test": "run-s test:unit test:e2e",
|
||||
"test:unit": "NODE_ENV=test mocha",
|
||||
"test:unit:watch": "NODE_ENV=test mocha --watch --inspect=5556",
|
||||
"test:unit:coverage": "NODE_ENV=test nyc --reporter=lcov --reporter=text --reporter=text-summary mocha",
|
||||
"test:e2e": "run-p --race start cypress:run",
|
||||
"js:watch": "NODE_ENV=development node server.js",
|
||||
"js:build": "webpack --config webpack.config.prod.js",
|
||||
|
|
File diff suppressed because it is too large
Load diff
6
public/assets/scripts/choices.min.js
vendored
6
public/assets/scripts/choices.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -78,7 +78,8 @@ a:focus {
|
|||
border-radius: 2.5px;
|
||||
font-size: 14px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
|
|
2
public/assets/styles/base.min.css
vendored
2
public/assets/styles/base.min.css
vendored
|
@ -1 +1 @@
|
|||
*{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}*,:after,:before{box-sizing:border-box}body,html{position:relative;margin:0;width:100%;height:100%}body{font-family:'Helvetica Neue',Helvetica,Arial,'Lucida Grande',sans-serif;font-size:16px;line-height:1.4;color:#fff;background-color:#333;overflow-x:hidden}hr,label{display:block}label,p{margin-bottom:8px}label{font-size:14px;font-weight:500;cursor:pointer}p{margin-top:0}hr{margin:30px 0;border:0;border-bottom:1px solid #eaeaea;height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:12px;font-weight:400;line-height:1.2}a,a:focus,a:visited{color:#fff;text-decoration:none;font-weight:600}.form-control{display:block;width:100%;background-color:#f9f9f9;padding:12px;border:1px solid #ddd;border-radius:2.5px;font-size:14px;-webkit-appearance:none;appearance:none;margin-bottom:24px}.h1,h1{font-size:32px}.h2,h2{font-size:24px}.h3,h3{font-size:20px}.h4,h4{font-size:18px}.h5,h5{font-size:16px}.h6,h6{font-size:14px}label+p{margin-top:-4px}.container{display:block;margin:auto;max-width:40em;padding:48px}@media (max-width:620px){.container{padding:0}}.section{background-color:#fff;padding:24px;color:#333}.section a,.section a:focus,.section a:visited{color:#00bcd4}.logo{display:block;margin-bottom:12px}.logo__img{width:100%;height:auto;display:inline-block;max-width:100%;vertical-align:top;padding:6px 0}.visible-ie{display:none}.push-bottom{margin-bottom:24px}.zero-bottom{margin-bottom:0}.zero-top{margin-top:0}.text-center{text-align:center}[data-test-hook]{margin-bottom:24px}
|
||||
*{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}*,:after,:before{box-sizing:border-box}body,html{position:relative;margin:0;width:100%;height:100%}body{font-family:'Helvetica Neue',Helvetica,Arial,'Lucida Grande',sans-serif;font-size:16px;line-height:1.4;color:#fff;background-color:#333;overflow-x:hidden}hr,label{display:block}label,p{margin-bottom:8px}label{font-size:14px;font-weight:500;cursor:pointer}p{margin-top:0}hr{margin:30px 0;border:0;border-bottom:1px solid #eaeaea;height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:12px;font-weight:400;line-height:1.2}a,a:focus,a:visited{color:#fff;text-decoration:none;font-weight:600}.form-control{display:block;width:100%;background-color:#f9f9f9;padding:12px;border:1px solid #ddd;border-radius:2.5px;font-size:14px;-webkit-appearance:none;-moz-appearance:none;appearance:none;margin-bottom:24px}.h1,h1{font-size:32px}.h2,h2{font-size:24px}.h3,h3{font-size:20px}.h4,h4{font-size:18px}.h5,h5{font-size:16px}.h6,h6{font-size:14px}label+p{margin-top:-4px}.container{display:block;margin:auto;max-width:40em;padding:48px}@media (max-width:620px){.container{padding:0}}.section{background-color:#fff;padding:24px;color:#333}.section a,.section a:focus,.section a:visited{color:#00bcd4}.logo{display:block;margin-bottom:12px}.logo__img{width:100%;height:auto;display:inline-block;max-width:100%;vertical-align:top;padding:6px 0}.visible-ie{display:none}.push-bottom{margin-bottom:24px}.zero-bottom{margin-bottom:0}.zero-top{margin-top:0}.text-center{text-align:center}[data-test-hook]{margin-bottom:24px}
|
|
@ -325,7 +325,8 @@
|
|||
.choices__button {
|
||||
text-indent: -9999px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
background-repeat: no-repeat;
|
||||
|
|
2
public/assets/styles/choices.min.css
vendored
2
public/assets/styles/choices.min.css
vendored
File diff suppressed because one or more lines are too long
|
@ -548,6 +548,15 @@
|
|||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var genericExamples = document.querySelectorAll('[data-trigger]');
|
||||
for (i = 0; i < genericExamples.length; ++i) {
|
||||
var element = genericExamples[i];
|
||||
new Choices(element, {
|
||||
placeholderValue: 'This is a placeholder set in the config',
|
||||
searchPlaceholderValue: 'This is a search placeholder',
|
||||
});
|
||||
}
|
||||
|
||||
var textRemove = new Choices(
|
||||
document.getElementById('choices-text-remove-button'),
|
||||
{
|
||||
|
|
|
@ -48,10 +48,6 @@ const USER_DEFAULTS = /** @type {Partial<import('../../types/index').Choices.Opt
|
|||
* @typedef {import('../../types/index').Choices.Choice} Choice
|
||||
*/
|
||||
class Choices {
|
||||
/* ========================================
|
||||
= Static properties =
|
||||
======================================== */
|
||||
|
||||
static get defaults() {
|
||||
return Object.preventExtensions({
|
||||
get options() {
|
||||
|
@ -63,17 +59,11 @@ class Choices {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @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.defaults.options, userConfig],
|
||||
// When merging array configs, replace with a copy of the userConfig array,
|
||||
|
@ -90,6 +80,7 @@ class Choices {
|
|||
userConfig.addItemFilter instanceof RegExp
|
||||
? userConfig.addItemFilter
|
||||
: new RegExp(userConfig.addItemFilter);
|
||||
|
||||
this.config.addItemFilter = re.test.bind(re);
|
||||
}
|
||||
|
||||
|
@ -105,19 +96,18 @@ class Choices {
|
|||
this.config.renderSelectedChoices = 'auto';
|
||||
}
|
||||
|
||||
// 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',
|
||||
);
|
||||
}
|
||||
|
||||
this._isTextElement = passedElement.type === 'text';
|
||||
|
@ -132,21 +122,17 @@ class Choices {
|
|||
classNames: this.config.classNames,
|
||||
delimiter: this.config.delimiter,
|
||||
});
|
||||
} 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 = {};
|
||||
this._currentState = {};
|
||||
this._prevState = {};
|
||||
|
@ -205,29 +191,31 @@ class Choices {
|
|||
this._onDirectionKey = this._onDirectionKey.bind(this);
|
||||
this._onDeleteKey = this._onDeleteKey.bind(this);
|
||||
|
||||
if (!this.config.silent) {
|
||||
if (this.config.shouldSortItems === true && this._isSelectOneElement) {
|
||||
if (this.config.shouldSortItems === true && this._isSelectOneElement) {
|
||||
if (!this.config.silent) {
|
||||
console.warn(
|
||||
"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(
|
||||
'Trying to initialise Choices on element already initialised',
|
||||
);
|
||||
}
|
||||
|
||||
this.initialised = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Let's go
|
||||
this.init();
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
= Public methods =
|
||||
======================================== */
|
||||
|
||||
init() {
|
||||
if (this.initialised) {
|
||||
return;
|
||||
|
@ -256,7 +244,7 @@ class Choices {
|
|||
|
||||
const { callbackOnInit } = this.config;
|
||||
// Run callback if it is a function
|
||||
if (callbackOnInit && isType('Function', callbackOnInit)) {
|
||||
if (callbackOnInit && typeof callbackOnInit === 'function') {
|
||||
callbackOnInit.call(this);
|
||||
}
|
||||
}
|
||||
|
@ -275,8 +263,7 @@ class Choices {
|
|||
}
|
||||
|
||||
this.clearStore();
|
||||
|
||||
this.config.templates = null;
|
||||
this._templates = null;
|
||||
this.initialised = false;
|
||||
}
|
||||
|
||||
|
@ -459,7 +446,7 @@ class Choices {
|
|||
}
|
||||
|
||||
// If only one value has been passed, convert to array
|
||||
const choiceValue = isType('Array', value) ? value : [value];
|
||||
const choiceValue = Array.isArray(value) ? value : [value];
|
||||
|
||||
// Loop through each value and
|
||||
choiceValue.forEach(val => this._findAndSelectChoiceByValue(val));
|
||||
|
@ -649,12 +636,6 @@ class Choices {
|
|||
return this;
|
||||
}
|
||||
|
||||
/* ===== End of Public methods ====== */
|
||||
|
||||
/* =============================================
|
||||
= Private functions =
|
||||
============================================= */
|
||||
|
||||
_render() {
|
||||
if (this._store.isLoading()) {
|
||||
return;
|
||||
|
@ -743,15 +724,17 @@ class Choices {
|
|||
let notice;
|
||||
|
||||
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;
|
||||
|
||||
dropdownItem = this._getTemplate('notice', notice, 'no-choices');
|
||||
}
|
||||
|
@ -1126,9 +1109,10 @@ class Choices {
|
|||
|
||||
_canAddItem(activeItems, value) {
|
||||
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;
|
||||
|
||||
if (!this._isSelectOneElement) {
|
||||
const isDuplicateValue = existsInArray(activeItems, value);
|
||||
|
@ -1140,9 +1124,10 @@ class Choices {
|
|||
// If there is a max entry limit and we have reached that limit
|
||||
// don't update
|
||||
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;
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -1151,9 +1136,10 @@ class Choices {
|
|||
canAddItem
|
||||
) {
|
||||
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;
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -1178,10 +1164,11 @@ class Choices {
|
|||
}
|
||||
|
||||
_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;
|
||||
|
||||
if (newValue.length < 1 && newValue === `${currentValue} `) {
|
||||
return 0;
|
||||
|
@ -1307,7 +1294,7 @@ class Choices {
|
|||
const { activeItems } = this._store;
|
||||
const hasFocusedInput = this.input.isFocussed;
|
||||
const hasActiveDropdown = this.dropdown.isActive;
|
||||
const hasItems = this.itemList.hasChildren;
|
||||
const hasItems = this.itemList.hasChildren();
|
||||
const keyString = String.fromCharCode(keyCode);
|
||||
|
||||
const {
|
||||
|
@ -1560,7 +1547,10 @@ class Choices {
|
|||
_onMouseDown(event) {
|
||||
const { target, shiftKey } = event;
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
@ -1777,7 +1767,7 @@ class Choices {
|
|||
placeholder = false,
|
||||
keyCode = null,
|
||||
}) {
|
||||
let passedValue = isType('String', value) ? value.trim() : value;
|
||||
let passedValue = typeof value === 'string' ? value.trim() : value;
|
||||
|
||||
const passedKeyCode = keyCode;
|
||||
const passedCustomProperties = customProperties;
|
||||
|
@ -1939,9 +1929,9 @@ class Choices {
|
|||
return null;
|
||||
}
|
||||
|
||||
const { templates, classNames } = this.config;
|
||||
const { classNames } = this.config;
|
||||
|
||||
return templates[template].call(this, classNames, ...args);
|
||||
return this._templates[template].call(this, classNames, ...args);
|
||||
}
|
||||
|
||||
_createTemplates() {
|
||||
|
@ -1950,12 +1940,12 @@ class Choices {
|
|||
|
||||
if (
|
||||
callbackOnCreateTemplates &&
|
||||
isType('Function', callbackOnCreateTemplates)
|
||||
typeof callbackOnCreateTemplates === 'function'
|
||||
) {
|
||||
userTemplates = callbackOnCreateTemplates.call(this, strToEl);
|
||||
}
|
||||
|
||||
this.config.templates = merge(TEMPLATES, userTemplates);
|
||||
this._templates = merge(TEMPLATES, userTemplates);
|
||||
}
|
||||
|
||||
_createElements() {
|
||||
|
@ -2230,17 +2220,6 @@ class Choices {
|
|||
}
|
||||
}
|
||||
|
||||
_generateInstances(elements, config) {
|
||||
return elements.reduce(
|
||||
(instances, element) => {
|
||||
instances.push(new Choices(element, config));
|
||||
|
||||
return instances;
|
||||
},
|
||||
[this],
|
||||
);
|
||||
}
|
||||
|
||||
_generatePlaceholderValue() {
|
||||
if (this._isSelectOneElement) {
|
||||
return false;
|
||||
|
|
|
@ -2,19 +2,14 @@ import { expect } from 'chai';
|
|||
import { spy, stub } from 'sinon';
|
||||
|
||||
import Choices from './choices';
|
||||
import { EVENTS, ACTION_TYPES } from './constants';
|
||||
import { EVENTS, ACTION_TYPES, DEFAULT_CONFIG } from './constants';
|
||||
import { WrappedSelect, WrappedInput } from './components/index';
|
||||
|
||||
describe('choices', () => {
|
||||
let instance;
|
||||
let output;
|
||||
let passedElement;
|
||||
|
||||
const returnsInstance = () => {
|
||||
it('returns this', () => {
|
||||
expect(output).to.eql(instance);
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
passedElement = document.createElement('input');
|
||||
passedElement.type = 'text';
|
||||
|
@ -24,12 +19,198 @@ describe('choices', () => {
|
|||
instance = new Choices(passedElement);
|
||||
});
|
||||
|
||||
describe('public methods', () => {
|
||||
afterEach(() => {
|
||||
output = null;
|
||||
instance = null;
|
||||
afterEach(() => {
|
||||
output = null;
|
||||
instance = null;
|
||||
});
|
||||
|
||||
const returnsInstance = () => {
|
||||
it('returns this', () => {
|
||||
expect(output).to.eql(instance);
|
||||
});
|
||||
};
|
||||
|
||||
describe('constructor', () => {
|
||||
describe('config', () => {
|
||||
describe('not passing config options', () => {
|
||||
it('uses the default config', () => {
|
||||
document.body.innerHTML = `
|
||||
<input data-choice type="text" id="input-1" />
|
||||
`;
|
||||
|
||||
instance = new Choices();
|
||||
|
||||
expect(instance.config).to.eql(DEFAULT_CONFIG);
|
||||
});
|
||||
});
|
||||
|
||||
describe('passing config options', () => {
|
||||
it('merges the passed config with the default config', () => {
|
||||
document.body.innerHTML = `
|
||||
<input data-choice type="text" id="input-1" />
|
||||
`;
|
||||
|
||||
const config = {
|
||||
renderChoiceLimit: 5,
|
||||
};
|
||||
instance = new Choices('[data-choice]', config);
|
||||
|
||||
expect(instance.config).to.eql({
|
||||
...DEFAULT_CONFIG,
|
||||
...config,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('not passing an element', () => {
|
||||
it('returns a Choices instance for the first element with a "data-choice" attribute', () => {
|
||||
document.body.innerHTML = `
|
||||
<input data-choice type="text" id="input-1" />
|
||||
<input data-choice type="text" id="input-2" />
|
||||
<input data-choice type="text" id="input-3" />
|
||||
`;
|
||||
|
||||
const inputs = document.querySelectorAll('[data-choice]');
|
||||
expect(inputs.length).to.equal(3);
|
||||
|
||||
instance = new Choices();
|
||||
|
||||
expect(instance.passedElement.element.id).to.equal(inputs[0].id);
|
||||
});
|
||||
|
||||
describe('when an element cannot be found in the DOM', () => {
|
||||
it('throws an error', () => {
|
||||
document.body.innerHTML = ``;
|
||||
expect(() => new Choices()).to.throw(
|
||||
TypeError,
|
||||
'Expected one of the following types text|select-one|select-multiple',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('passing an element', () => {
|
||||
describe('passing an element that has not been initialised with Choices', () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
<input type="text" id="input-1" />
|
||||
`;
|
||||
});
|
||||
|
||||
it('sets the initialised flag to true', () => {
|
||||
instance = new Choices('#input-1');
|
||||
expect(instance.initialised).to.equal(true);
|
||||
});
|
||||
|
||||
it('intialises', () => {
|
||||
const initSpy = spy();
|
||||
// initialise with the same element
|
||||
instance = new Choices('#input-1', {
|
||||
silent: true,
|
||||
callbackOnInit: initSpy,
|
||||
});
|
||||
|
||||
expect(initSpy.called).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('passing an element that has already be initialised with Choices', () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
<input type="text" id="input-1" />
|
||||
`;
|
||||
|
||||
// initialise once
|
||||
new Choices('#input-1', { silent: true });
|
||||
});
|
||||
|
||||
it('sets the initialised flag to true', () => {
|
||||
// initialise with the same element
|
||||
instance = new Choices('#input-1', { silent: true });
|
||||
|
||||
expect(instance.initialised).to.equal(true);
|
||||
});
|
||||
|
||||
it('does not reinitialise', () => {
|
||||
const initSpy = spy();
|
||||
// initialise with the same element
|
||||
instance = new Choices('#input-1', {
|
||||
silent: true,
|
||||
callbackOnInit: initSpy,
|
||||
});
|
||||
|
||||
expect(initSpy.called).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`passing an element as a DOMString`, () => {
|
||||
describe('passing a input element type', () => {
|
||||
it('sets the "passedElement" instance property as an instance of WrappedInput', () => {
|
||||
document.body.innerHTML = `
|
||||
<input data-choice type="text" id="input-1" />
|
||||
`;
|
||||
|
||||
instance = new Choices('[data-choice]');
|
||||
|
||||
expect(instance.passedElement).to.be.an.instanceOf(WrappedInput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('passing a select element type', () => {
|
||||
it('sets the "passedElement" instance property as an instance of WrappedSelect', () => {
|
||||
document.body.innerHTML = `
|
||||
<select data-choice id="select-1"></select>
|
||||
`;
|
||||
|
||||
instance = new Choices('[data-choice]');
|
||||
|
||||
expect(instance.passedElement).to.be.an.instanceOf(WrappedSelect);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(`passing an element as a HTMLElement`, () => {
|
||||
describe('passing a input element type', () => {
|
||||
it('sets the "passedElement" instance property as an instance of WrappedInput', () => {
|
||||
document.body.innerHTML = `
|
||||
<input data-choice type="text" id="input-1" />
|
||||
`;
|
||||
|
||||
instance = new Choices(document.querySelector('[data-choice]'));
|
||||
|
||||
expect(instance.passedElement).to.be.an.instanceOf(WrappedInput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('passing a select element type', () => {
|
||||
it('sets the "passedElement" instance property as an instance of WrappedSelect', () => {
|
||||
document.body.innerHTML = `
|
||||
<select data-choice id="select-1"></select>
|
||||
`;
|
||||
|
||||
instance = new Choices(document.querySelector('[data-choice]'));
|
||||
|
||||
expect(instance.passedElement).to.be.an.instanceOf(WrappedSelect);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('passing an invalid element type', () => {
|
||||
it('throws an TypeError', () => {
|
||||
document.body.innerHTML = `
|
||||
<div data-choice id="div-1"></div>
|
||||
`;
|
||||
expect(() => new Choices('[data-choice]')).to.throw(
|
||||
TypeError,
|
||||
'Expected one of the following types text|select-one|select-multiple',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('public methods', () => {
|
||||
describe('init', () => {
|
||||
const callbackOnInitSpy = spy();
|
||||
|
||||
|
@ -172,7 +353,7 @@ describe('choices', () => {
|
|||
});
|
||||
|
||||
it('nullifys templates config', () => {
|
||||
expect(instance.config.templates).to.equal(null);
|
||||
expect(instance._templates).to.equal(null);
|
||||
});
|
||||
|
||||
it('resets initialise flag', () => {
|
||||
|
|
|
@ -6,7 +6,6 @@ export default class List {
|
|||
|
||||
this.scrollPos = this.element.scrollTop;
|
||||
this.height = this.element.offsetHeight;
|
||||
this.hasChildren = !!this.element.children;
|
||||
}
|
||||
|
||||
clear() {
|
||||
|
@ -21,6 +20,10 @@ export default class List {
|
|||
return this.element.querySelector(selector);
|
||||
}
|
||||
|
||||
hasChildren() {
|
||||
return this.element.hasChildNodes();
|
||||
}
|
||||
|
||||
scrollToTop() {
|
||||
this.element.scrollTop = 0;
|
||||
}
|
||||
|
@ -37,52 +40,52 @@ export default class List {
|
|||
// Scroll position of dropdown
|
||||
const containerScrollPos = this.element.scrollTop + dropdownHeight;
|
||||
// Difference between the choice and scroll position
|
||||
const endpoint =
|
||||
const destination =
|
||||
direction > 0
|
||||
? this.element.scrollTop + choicePos - containerScrollPos
|
||||
: choice.offsetTop;
|
||||
|
||||
requestAnimationFrame(time => {
|
||||
this._animateScroll(time, endpoint, direction);
|
||||
this._animateScroll(time, destination, direction);
|
||||
});
|
||||
}
|
||||
|
||||
_scrollDown(scrollPos, strength, endpoint) {
|
||||
const easing = (endpoint - scrollPos) / strength;
|
||||
_scrollDown(scrollPos, strength, destination) {
|
||||
const easing = (destination - scrollPos) / strength;
|
||||
const distance = easing > 1 ? easing : 1;
|
||||
|
||||
this.element.scrollTop = scrollPos + distance;
|
||||
}
|
||||
|
||||
_scrollUp(scrollPos, strength, endpoint) {
|
||||
const easing = (scrollPos - endpoint) / strength;
|
||||
_scrollUp(scrollPos, strength, destination) {
|
||||
const easing = (scrollPos - destination) / strength;
|
||||
const distance = easing > 1 ? easing : 1;
|
||||
|
||||
this.element.scrollTop = scrollPos - distance;
|
||||
}
|
||||
|
||||
_animateScroll(time, endpoint, direction) {
|
||||
_animateScroll(time, destination, direction) {
|
||||
const strength = SCROLLING_SPEED;
|
||||
const choiceListScrollTop = this.element.scrollTop;
|
||||
let continueAnimation = false;
|
||||
|
||||
if (direction > 0) {
|
||||
this._scrollDown(choiceListScrollTop, strength, endpoint);
|
||||
this._scrollDown(choiceListScrollTop, strength, destination);
|
||||
|
||||
if (choiceListScrollTop < endpoint) {
|
||||
if (choiceListScrollTop < destination) {
|
||||
continueAnimation = true;
|
||||
}
|
||||
} else {
|
||||
this._scrollUp(choiceListScrollTop, strength, endpoint);
|
||||
this._scrollUp(choiceListScrollTop, strength, destination);
|
||||
|
||||
if (choiceListScrollTop > endpoint) {
|
||||
if (choiceListScrollTop > destination) {
|
||||
continueAnimation = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (continueAnimation) {
|
||||
requestAnimationFrame(() => {
|
||||
this._animateScroll(time, endpoint, direction);
|
||||
this._animateScroll(time, destination, direction);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,10 @@ describe('components/list', () => {
|
|||
it('assigns choices element to class', () => {
|
||||
expect(instance.element).to.eql(choicesElement);
|
||||
});
|
||||
|
||||
it('sets the height of the element', () => {
|
||||
expect(instance.height).to.eql(choicesElement.scrollTop);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
|
@ -62,4 +66,31 @@ describe('components/list', () => {
|
|||
expect(expectedResponse).to.eql(actualResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasChildren', () => {
|
||||
describe('when list has children', () => {
|
||||
it('returns true', () => {
|
||||
const childElement = document.createElement('span');
|
||||
instance.element.appendChild(childElement);
|
||||
const response = instance.hasChildren();
|
||||
expect(response).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when list does not have children', () => {
|
||||
it('returns false', () => {
|
||||
instance.element.innerHTML = '';
|
||||
const response = instance.hasChildren();
|
||||
expect(response).to.equal(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('scrollToTop', () => {
|
||||
it("sets the position's scroll position to 0", () => {
|
||||
instance.element.scrollTop = 10;
|
||||
instance.scrollToTop();
|
||||
expect(instance.element.scrollTop).to.equal(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { dispatchEvent, isElement } from '../lib/utils';
|
||||
import { dispatchEvent } from '../lib/utils';
|
||||
|
||||
export default class WrappedElement {
|
||||
constructor({ element, classNames }) {
|
||||
Object.assign(this, { element, classNames });
|
||||
|
||||
if (!isElement(element)) {
|
||||
if (!(element instanceof Element)) {
|
||||
throw new TypeError('Invalid element passed');
|
||||
}
|
||||
|
||||
|
|
|
@ -28,8 +28,6 @@ export const getType = obj => Object.prototype.toString.call(obj).slice(8, -1);
|
|||
export const isType = (type, obj) =>
|
||||
obj !== undefined && obj !== null && getType(obj) === type;
|
||||
|
||||
export const isElement = element => element instanceof Element;
|
||||
|
||||
export const wrap = (element, wrapper = document.createElement('div')) => {
|
||||
if (element.nextSibling) {
|
||||
element.parentNode.insertBefore(wrapper, element.nextSibling);
|
||||
|
@ -80,7 +78,7 @@ export const isScrolledIntoView = (el, parent, direction = 1) => {
|
|||
};
|
||||
|
||||
export const sanitise = value => {
|
||||
if (!isType('String', value)) {
|
||||
if (typeof value !== 'string') {
|
||||
return value;
|
||||
}
|
||||
|
||||
|
@ -145,15 +143,12 @@ export const getWindowHeight = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export const isIE11 = () =>
|
||||
!!(
|
||||
navigator.userAgent.match(/Trident/) &&
|
||||
navigator.userAgent.match(/rv[ :]11/)
|
||||
);
|
||||
export const isIE11 = userAgent =>
|
||||
!!(userAgent.match(/Trident/) && userAgent.match(/rv[ :]11/));
|
||||
|
||||
export const existsInArray = (array, value, key = 'value') =>
|
||||
array.some(item => {
|
||||
if (isType('String', value)) {
|
||||
if (typeof value === 'string') {
|
||||
return item[key] === value.trim();
|
||||
}
|
||||
|
||||
|
|
|
@ -6,10 +6,10 @@ import {
|
|||
generateId,
|
||||
getType,
|
||||
isType,
|
||||
isElement,
|
||||
sanitise,
|
||||
sortByAlpha,
|
||||
sortByScore,
|
||||
isIE11,
|
||||
existsInArray,
|
||||
cloneObject,
|
||||
dispatchEvent,
|
||||
|
@ -96,14 +96,6 @@ describe('utils', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('isElement', () => {
|
||||
it('checks with given object is an element', () => {
|
||||
const element = document.createElement('div');
|
||||
expect(isElement(element)).to.equal(true);
|
||||
expect(isElement({})).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitise', () => {
|
||||
it('strips HTML from value', () => {
|
||||
const value = '<script>somethingMalicious();</script>';
|
||||
|
@ -197,6 +189,18 @@ describe('utils', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('isIE11', () => {
|
||||
it('returns whether the given user agent string matches an IE11 user agent string', () => {
|
||||
const IE11UserAgent =
|
||||
'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko';
|
||||
const firefoxUserAgent =
|
||||
'Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0';
|
||||
|
||||
expect(isIE11(IE11UserAgent)).to.equal(true);
|
||||
expect(isIE11(firefoxUserAgent)).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('existsInArray', () => {
|
||||
it('determines whether a value exists within given array', () => {
|
||||
const values = [
|
||||
|
|
10
types/index.d.ts
vendored
10
types/index.d.ts
vendored
|
@ -777,16 +777,6 @@ export default class Choices {
|
|||
userConfig?: Partial<Choices.Options>,
|
||||
);
|
||||
|
||||
/**
|
||||
* It's impossible to declare in TypeScript what Choices constructor is actually doing:
|
||||
* @see {@link https://github.com/Microsoft/TypeScript/issues/27594}
|
||||
* it returns array of Choices in case if selectorOrElement is string
|
||||
* and one instance of Choices otherwise
|
||||
* This little hack will at least allow to use it in Typescript
|
||||
*
|
||||
*/
|
||||
[index: number]: this;
|
||||
|
||||
/**
|
||||
* Creates a new instance of Choices, adds event listeners, creates templates and renders a Choices element to the DOM.
|
||||
*
|
||||
|
|
Loading…
Reference in a new issue