-
-
+
+
+
@@ -64,11 +64,6 @@
-
-
-
-
-
@@ -117,20 +112,19 @@
maxItemCount: 5,
});
- new Choices('#choices-regex-filter', {
- regexFilter: /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
+ new Choices('#choices-add-item-filter', {
+ addItems: true,
+ addItemFilterFn: (value) => {
+ const regex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
+ const expression = new RegExp(regex.source, 'i');
+ return expression.test(value);
+ },
});
new Choices('#choices-adding-items-disabled', {
addItems: false,
});
- new Choices('#choices-add-item-callback', {
- addItemFilter: function (value) {
- return (value !== 'test')
- }
- });
-
new Choices('#choices-disabled-via-attr');
new Choices('#choices-prepend-append', {
diff --git a/src/scripts/choices.js b/src/scripts/choices.js
index 170c968..bcb8096 100644
--- a/src/scripts/choices.js
+++ b/src/scripts/choices.js
@@ -33,12 +33,11 @@ import {
sortByScore,
generateId,
findAncestorByAttrName,
- regexFilter,
fetchFromObject,
isIE11,
existsInArray,
cloneObject,
- doKeysMatch,
+ diff,
} from './lib/utils';
/**
@@ -64,8 +63,12 @@ class Choices {
{ arrayMerge: (destinationArray, sourceArray) => [...sourceArray] },
);
- if (!doKeysMatch(this.config, DEFAULT_CONFIG)) {
- console.warn('Unknown config option(s) passed');
+ const invalidConfigOptions = diff(this.config, DEFAULT_CONFIG);
+ if (invalidConfigOptions.length) {
+ console.warn(
+ 'Unknown config option(s) passed',
+ invalidConfigOptions.join(', '),
+ );
}
if (!['auto', 'always'].includes(this.config.renderSelectedChoices)) {
@@ -375,11 +378,6 @@ class Choices {
return this;
}
- toggleDropdown() {
- this.dropdown.isActive ? this.hideDropdown() : this.showDropdown();
- return this;
- }
-
getValue(valueOnly = false) {
const values = this._store.activeItems.reduce((selectedItems, item) => {
const itemValue = valueOnly ? item.value : item;
@@ -965,18 +963,6 @@ class Choices {
: this.config.maxItemText;
}
- if (
- this.config.regexFilter &&
- this._isTextElement &&
- this.config.addItems &&
- canAddItem
- ) {
- // If a user has supplied a regular expression filter
- // determine whether we can update based on whether
- // our regular expression passes
- canAddItem = regexFilter(value, this.config.regexFilter);
- }
-
if (
!this.config.duplicateItemsAllowed &&
isDuplicateValue &&
@@ -989,11 +975,11 @@ class Choices {
}
if (
- isType('Function', this.config.addItemFilter) &&
- this.config.addItemFilter(value) &&
this._isTextElement &&
this.config.addItems &&
- canAddItem
+ canAddItem &&
+ isType('Function', this.config.addItemFilterFn) &&
+ !this.config.addItemFilterFn(value)
) {
canAddItem = false;
notice = isType('Function', this.config.customAddItemText)
@@ -1202,35 +1188,29 @@ class Choices {
const value = this.input.value;
const activeItems = this._store.activeItems;
const canAddItem = this._canAddItem(activeItems, value);
+ const { BACK_KEY: backKey, DELETE_KEY: deleteKey } = KEY_CODES;
// We are typing into a text input and have a value, we want to show a dropdown
// notice. Otherwise hide the dropdown
if (this._isTextElement) {
- if (value) {
- if (canAddItem.notice) {
- const dropdownItem = this._getTemplate('notice', canAddItem.notice);
- this.dropdown.element.innerHTML = dropdownItem.outerHTML;
- }
-
- if (canAddItem.response === true) {
- this.showDropdown(true);
- } else if (!canAddItem.notice) {
- this.hideDropdown(true);
- }
+ const canShowDropdownNotice = canAddItem.notice && value;
+ if (canShowDropdownNotice) {
+ const dropdownItem = this._getTemplate('notice', canAddItem.notice);
+ this.dropdown.element.innerHTML = dropdownItem.outerHTML;
+ this.showDropdown(true);
} else {
this.hideDropdown(true);
}
} else {
- const backKey = KEY_CODES.BACK_KEY;
- const deleteKey = KEY_CODES.DELETE_KEY;
+ const userHasRemovedValue =
+ (keyCode === backKey || keyCode === deleteKey) && !target.value;
+ const canReactivateChoices = !this._isTextElement && this._isSearching;
+ const canSearch = this._canSearch && canAddItem.response;
- // If user has removed value...
- if ((keyCode === backKey || keyCode === deleteKey) && !target.value) {
- if (!this._isTextElement && this._isSearching) {
- this._isSearching = false;
- this._store.dispatch(activateChoices(true));
- }
- } else if (this._canSearch && canAddItem.response) {
+ if (userHasRemovedValue && canReactivateChoices) {
+ this._isSearching = false;
+ this._store.dispatch(activateChoices(true));
+ } else if (canSearch) {
this._handleSearch(this.input.value);
}
}
@@ -1242,12 +1222,13 @@ class Choices {
// If CTRL + A or CMD + A have been pressed and there are items to select
if (hasCtrlDownKeyPressed && hasItems) {
this._canSearch = false;
- if (
+
+ const shouldHightlightAll =
this.config.removeItems &&
!this.input.value &&
- this.input.element === document.activeElement
- ) {
- // Highlight items
+ this.input.element === document.activeElement;
+
+ if (shouldHightlightAll) {
this.highlightAll();
}
}
@@ -1255,12 +1236,12 @@ class Choices {
_onEnterKey({ event, target, activeItems, hasActiveDropdown }) {
const { ENTER_KEY: enterKey } = KEY_CODES;
- // If enter key is pressed and the input has a value
+ const targetWasButton = target.hasAttribute('data-button');
+
if (this._isTextElement && target.value) {
const value = this.input.value;
const canAddItem = this._canAddItem(activeItems, value);
- // All is good, add
if (canAddItem.response) {
this.hideDropdown(true);
this._addItem({ value });
@@ -1269,28 +1250,26 @@ class Choices {
}
}
- if (target.hasAttribute('data-button')) {
+ if (targetWasButton) {
this._handleButtonAction(activeItems, target);
event.preventDefault();
}
if (hasActiveDropdown) {
- const highlighted = this.dropdown.getChild(
+ const highlightedChoice = this.dropdown.getChild(
`.${this.config.classNames.highlightedState}`,
);
- // If we have a highlighted choice
- if (highlighted) {
+ if (highlightedChoice) {
// add enter keyCode value
if (activeItems[0]) {
activeItems[0].keyCode = enterKey; // eslint-disable-line no-param-reassign
}
- this._handleChoiceAction(activeItems, highlighted);
+ this._handleChoiceAction(activeItems, highlightedChoice);
}
event.preventDefault();
} else if (this._isSelectOneElement) {
- // Open single select dropdown if it's not active
this.showDropdown();
event.preventDefault();
}
@@ -1375,31 +1354,29 @@ class Choices {
}
_onTouchMove() {
- if (this._wasTap === true) {
+ if (this._wasTap) {
this._wasTap = false;
}
}
_onTouchEnd(event) {
- const target = event.target || event.touches[0].target;
+ const { target } = event || event.touches[0];
+ const touchWasWithinContainer =
+ this._wasTap && this.containerOuter.element.contains(target);
- // If a user tapped within our container...
- if (this._wasTap === true && this.containerOuter.element.contains(target)) {
- // ...and we aren't dealing with a single select box, show dropdown/focus input
-
- const containerWasTarget =
+ if (touchWasWithinContainer) {
+ const containerWasExactTarget =
target === this.containerOuter.element ||
target === this.containerInner.element;
- if (containerWasTarget && !this._isSelectOneElement) {
+ if (containerWasExactTarget) {
if (this._isTextElement) {
- // If text element, we only want to focus the input
this.input.focus();
- } else {
- // If a select box, we want to show the dropdown
+ } else if (this._isSelectMultipleElement) {
this.showDropdown();
}
}
+
// Prevents focus event firing
event.stopPropagation();
}
@@ -1423,7 +1400,6 @@ class Choices {
const activeItems = this._store.activeItems;
const hasShiftKey = shiftKey;
-
const buttonTarget = findAncestorByAttrName(target, 'data-button');
const itemTarget = findAncestorByAttrName(target, 'data-item');
const choiceTarget = findAncestorByAttrName(target, 'data-choice');
@@ -1451,7 +1427,11 @@ class Choices {
}
_onClick({ target }) {
- if (this.containerOuter.element.contains(target)) {
+ const clickWasWithinContainer = this.containerOuter.element.contains(
+ target,
+ );
+
+ if (clickWasWithinContainer) {
if (!this.dropdown.isActive && !this.containerOuter.isDisabled) {
if (this._isTextElement) {
if (document.activeElement !== this.input.element) {
@@ -1481,7 +1461,11 @@ class Choices {
}
_onFocus({ target }) {
- if (!this.containerOuter.element.contains(target)) {
+ const focusWasWithinContainer = this.containerOuter.element.contains(
+ target,
+ );
+
+ if (!focusWasWithinContainer) {
return;
}
@@ -1511,11 +1495,9 @@ class Choices {
}
_onBlur({ target }) {
- // If target is something that concerns us
- if (
- this.containerOuter.element.contains(target) &&
- !this._isScrollingOnIe
- ) {
+ const blurWasWithinContainer = this.containerOuter.element.contains(target);
+
+ if (blurWasWithinContainer && !this._isScrollingOnIe) {
const activeItems = this._store.activeItems;
const hasHighlightedItems = activeItems.some(item => item.highlighted);
const blurActions = {
diff --git a/src/scripts/choices.test.js b/src/scripts/choices.test.js
index 01991cb..12c21fa 100644
--- a/src/scripts/choices.test.js
+++ b/src/scripts/choices.test.js
@@ -491,50 +491,6 @@ describe('choices', () => {
});
});
- describe('toggleDropdown', () => {
- let hideDropdownStub;
- let showDropdownStub;
-
- beforeEach(() => {
- hideDropdownStub = stub();
- showDropdownStub = stub();
-
- instance.hideDropdown = hideDropdownStub;
- instance.showDropdown = showDropdownStub;
- });
-
- afterEach(() => {
- instance.hideDropdown.reset();
- instance.showDropdown.reset();
- });
-
- describe('dropdown active', () => {
- beforeEach(() => {
- instance.dropdown.isActive = true;
- output = instance.toggleDropdown();
- });
-
- it('hides dropdown', () => {
- expect(hideDropdownStub.called).to.equal(true);
- });
-
- returnsInstance(output);
- });
-
- describe('dropdown inactive', () => {
- beforeEach(() => {
- instance.dropdown.isActive = false;
- output = instance.toggleDropdown();
- });
-
- it('shows dropdown', () => {
- expect(showDropdownStub.called).to.equal(true);
- });
-
- returnsInstance(output);
- });
- });
-
describe('highlightItem', () => {
let passedElementTriggerEventStub;
let storeDispatchSpy;
diff --git a/src/scripts/constants.js b/src/scripts/constants.js
index 64aac00..73b9707 100644
--- a/src/scripts/constants.js
+++ b/src/scripts/constants.js
@@ -36,6 +36,7 @@ export const DEFAULT_CONFIG = {
renderChoiceLimit: -1,
maxItemCount: -1,
addItems: true,
+ addItemFilterFn: null,
removeItems: true,
removeItemButton: false,
editItems: false,
@@ -49,7 +50,6 @@ export const DEFAULT_CONFIG = {
searchFields: ['label', 'value'],
position: 'auto',
resetScrollPosition: true,
- regexFilter: null,
shouldSort: true,
shouldSortItems: false,
sortFn: sortByAlpha,
@@ -64,7 +64,7 @@ export const DEFAULT_CONFIG = {
noChoicesText: 'No choices to choose from',
itemSelectText: 'Press to select',
uniqueItemText: 'Only unique values can be added',
- customAddItemText: 'Only values matching specific conditions can be added.',
+ customAddItemText: 'Only values matching specific conditions can be added',
addItemText: value => `Press Enter to add "${stripHTML(value)}"`,
maxItemText: maxItemCount => `Only ${maxItemCount} values can be added`,
itemComparer: (choice, item) => choice === item,
@@ -72,7 +72,6 @@ export const DEFAULT_CONFIG = {
includeScore: true,
},
callbackOnInit: null,
- addItemFilter: null,
callbackOnCreateTemplates: null,
classNames: DEFAULT_CLASSNAMES,
};
diff --git a/src/scripts/constants.test.js b/src/scripts/constants.test.js
index 01a1f6e..89623dc 100644
--- a/src/scripts/constants.test.js
+++ b/src/scripts/constants.test.js
@@ -56,6 +56,7 @@ describe('constants', () => {
expect(DEFAULT_CONFIG.renderChoiceLimit).to.be.a('number');
expect(DEFAULT_CONFIG.maxItemCount).to.be.a('number');
expect(DEFAULT_CONFIG.addItems).to.be.a('boolean');
+ expect(DEFAULT_CONFIG.addItemFilterFn).to.equal(null);
expect(DEFAULT_CONFIG.removeItems).to.be.a('boolean');
expect(DEFAULT_CONFIG.removeItemButton).to.be.a('boolean');
expect(DEFAULT_CONFIG.editItems).to.be.a('boolean');
@@ -68,7 +69,6 @@ describe('constants', () => {
expect(DEFAULT_CONFIG.searchResultLimit).to.be.a('number');
expect(DEFAULT_CONFIG.searchFields).to.be.an('array');
expect(DEFAULT_CONFIG.position).to.be.a('string');
- expect(DEFAULT_CONFIG.regexFilter).to.equal(null);
expect(DEFAULT_CONFIG.shouldSort).to.be.a('boolean');
expect(DEFAULT_CONFIG.shouldSortItems).to.be.a('boolean');
expect(DEFAULT_CONFIG.placeholder).to.be.a('boolean');
@@ -86,7 +86,6 @@ describe('constants', () => {
expect(DEFAULT_CONFIG.addItemText).to.be.a('function');
expect(DEFAULT_CONFIG.maxItemText).to.be.a('function');
expect(DEFAULT_CONFIG.fuseOptions).to.be.an('object');
- expect(DEFAULT_CONFIG.addItemFilter).to.equal(null);
expect(DEFAULT_CONFIG.callbackOnInit).to.equal(null);
expect(DEFAULT_CONFIG.callbackOnCreateTemplates).to.equal(null);
});
diff --git a/src/scripts/lib/utils.js b/src/scripts/lib/utils.js
index 14437a5..2eda11d 100644
--- a/src/scripts/lib/utils.js
+++ b/src/scripts/lib/utils.js
@@ -229,15 +229,6 @@ export const dispatchEvent = (element, type, customArgs = null) => {
return element.dispatchEvent(event);
};
-export const regexFilter = (value, regex) => {
- if (!value || !regex) {
- return false;
- }
-
- const expression = new RegExp(regex.source, 'i');
- return expression.test(value);
-};
-
export const getWindowHeight = () => {
const body = document.body;
const html = document.documentElement;
@@ -289,8 +280,11 @@ export const existsInArray = (array, value, key = 'value') =>
export const cloneObject = obj => JSON.parse(JSON.stringify(obj));
-export const doKeysMatch = (a, b) => {
+export const diff = (a, b) => {
const aKeys = Object.keys(a).sort();
const bKeys = Object.keys(b).sort();
- return JSON.stringify(aKeys) === JSON.stringify(bKeys);
+
+ return aKeys.filter((i) => {
+ return bKeys.indexOf(i) < 0;
+ });
}
diff --git a/src/scripts/lib/utils.test.js b/src/scripts/lib/utils.test.js
index b128d0a..53f5889 100644
--- a/src/scripts/lib/utils.test.js
+++ b/src/scripts/lib/utils.test.js
@@ -14,7 +14,6 @@ import {
fetchFromObject,
existsInArray,
cloneObject,
- regexFilter,
dispatchEvent,
} from './utils';
@@ -248,17 +247,6 @@ describe('utils', () => {
});
});
- describe('regexFilter', () => {
- it('tests given regex against given value', () => {
- // An email address regex
- // eslint-disable-next-line
- const regex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
-
- expect(regexFilter('joe@bloggs.com', regex)).to.equal(true);
- expect(regexFilter('joe bloggs', regex)).to.equal(false);
- });
- });
-
describe('reduceToValues', () => {
it('reduces an array of objects to an array of values using given key', () => {
const values = [
diff --git a/types/index.d.ts b/types/index.d.ts
index 6c8a987..626fa9d 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -29,6 +29,15 @@ declare namespace Choices {
*/
"addItem": CustomEvent;
+ /**
+ * A filter that will need to pass for a user to successfully add an item.
+ *
+ * **Input types affected:** text
+ *
+ * @default null
+ */
+ addItemFilterFn?: () => any;
+
/**
* Triggered each time an item is removed (programmatically or by the user).
*
@@ -405,15 +414,6 @@ declare namespace Choices {
*/
resetScrollPosition?: boolean;
- /**
- * A filter that will need to pass for a user to successfully add an item.
- *
- * **Input types affected:** text
- *
- * @default null
- */
- regexFilter?: RegExp;
-
/**
* Whether choices and groups should be sorted. If false, choices/groups will appear in the order they were given.
*
@@ -764,13 +764,6 @@ export default class Choices {
*/
hideDropdown(blurInput?: boolean): this;
- /**
- * Toggle dropdown between showing/hidden.
- *
- * **Input types affected:** text, select-multiple
- */
- toggleDropdown(): this;
-
/**
* Get value(s) of input (i.e. inputted items (text) or selected choices (select)). Optionally pass an argument of `true` to only return values rather than value objects.
*