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:
Josh Johnson 2019-10-29 21:19:56 +00:00 committed by GitHub
parent 88f63faa0b
commit 0e44a916e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 842 additions and 544 deletions

View file

@ -64,7 +64,8 @@
},
"rules": {
"no-restricted-syntax": "off",
"compat/compat": "off"
"compat/compat": "off",
"no-new": "off"
}
},
{

View file

@ -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

View file

@ -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

View file

@ -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: [],

View file

@ -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
View file

@ -1,6 +1,6 @@
{
"name": "choices.js",
"version": "7.1.5",
"version": "8.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View file

@ -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

File diff suppressed because one or more lines are too long

View file

@ -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;
}

View file

@ -1 +1 @@
*{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}*,:after,:before{box-sizing:border-box}body,html{position:relative;margin:0;width:100%;height:100%}body{font-family:'Helvetica Neue',Helvetica,Arial,'Lucida Grande',sans-serif;font-size:16px;line-height:1.4;color:#fff;background-color:#333;overflow-x:hidden}hr,label{display:block}label,p{margin-bottom:8px}label{font-size:14px;font-weight:500;cursor:pointer}p{margin-top:0}hr{margin:30px 0;border:0;border-bottom:1px solid #eaeaea;height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:12px;font-weight:400;line-height:1.2}a,a:focus,a:visited{color:#fff;text-decoration:none;font-weight:600}.form-control{display:block;width:100%;background-color:#f9f9f9;padding:12px;border:1px solid #ddd;border-radius:2.5px;font-size:14px;-webkit-appearance:none;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}

View file

@ -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;

File diff suppressed because one or more lines are too long

View file

@ -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'),
{

View file

@ -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;

View file

@ -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', () => {

View file

@ -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);
});
}
}

View file

@ -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);
});
});
});

View file

@ -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');
}

View file

@ -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();
}

View file

@ -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
View file

@ -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.
*