diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..ef8bdf1
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,9 @@
+
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
\ No newline at end of file
diff --git a/.eslintrc b/.eslintrc
index fac080d..c90fecf 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -10,7 +10,7 @@
"rules": {
"quotes": [2, "single"],
"strict": [2, "never"],
- "indent": ["error", 4, {"SwitchCase": 1}],
+ "indent": ["error", 2, {"SwitchCase": 1}],
"eol-last": "off",
"arrow-body-style": "off",
"no-underscore-dangle": "off",
diff --git a/assets/scripts/src/actions/index.js b/assets/scripts/src/actions/index.js
index f275eab..96464d5 100644
--- a/assets/scripts/src/actions/index.js
+++ b/assets/scripts/src/actions/index.js
@@ -1,67 +1,67 @@
export const addItem = (value, label, id, choiceId, activateOptions) => {
- return {
- type: 'ADD_ITEM',
- value,
- label,
- id,
- choiceId,
- activateOptions,
- };
+ return {
+ type: 'ADD_ITEM',
+ value,
+ label,
+ id,
+ choiceId,
+ activateOptions,
+ };
};
export const removeItem = (id, choiceId) => {
- return {
- type: 'REMOVE_ITEM',
- id,
- choiceId,
- };
+ return {
+ type: 'REMOVE_ITEM',
+ id,
+ choiceId,
+ };
};
export const highlightItem = (id, highlighted) => {
- return {
- type: 'HIGHLIGHT_ITEM',
- id,
- highlighted,
- };
+ return {
+ type: 'HIGHLIGHT_ITEM',
+ id,
+ highlighted,
+ };
};
export const addChoice = (value, label, id, groupId, disabled) => {
- return {
- type: 'ADD_CHOICE',
- value,
- label,
- id,
- groupId,
- disabled,
- };
+ return {
+ type: 'ADD_CHOICE',
+ value,
+ label,
+ id,
+ groupId,
+ disabled,
+ };
};
export const filterChoices = (results) => {
- return {
- type: 'FILTER_CHOICES',
- results,
- };
+ return {
+ type: 'FILTER_CHOICES',
+ results,
+ };
};
export const activateChoices = (active = true) => {
- return {
- type: 'ACTIVATE_CHOICES',
- active,
- };
+ return {
+ type: 'ACTIVATE_CHOICES',
+ active,
+ };
};
export const addGroup = (value, id, active, disabled) => {
- return {
- type: 'ADD_GROUP',
- value,
- id,
- active,
- disabled,
- };
+ return {
+ type: 'ADD_GROUP',
+ value,
+ id,
+ active,
+ disabled,
+ };
};
export const clearAll = () => {
- return {
- type: 'CLEAR_ALL',
- };
+ return {
+ type: 'CLEAR_ALL',
+ };
};
\ No newline at end of file
diff --git a/assets/scripts/src/choices.js b/assets/scripts/src/choices.js
index 9103513..f92d1f9 100644
--- a/assets/scripts/src/choices.js
+++ b/assets/scripts/src/choices.js
@@ -1,2078 +1,2085 @@
import Fuse from 'fuse.js';
import Store from './store/index.js';
import {
- addItem,
- removeItem,
- highlightItem,
- addChoice,
- filterChoices,
- activateChoices,
- addGroup,
- clearAll,
-} from './actions/index';
+ addItem,
+ removeItem,
+ highlightItem,
+ addChoice,
+ filterChoices,
+ activateChoices,
+ addGroup,
+ clearAll,
+}
+from './actions/index';
import {
- isScrolledIntoView,
- getAdjacentEl,
- wrap,
- isType,
- isElement,
- strToEl,
- extend,
- getWidthOfInput,
- sortByAlpha,
- sortByScore,
-} from './lib/utils.js';
+ isScrolledIntoView,
+ getAdjacentEl,
+ wrap,
+ isType,
+ isElement,
+ strToEl,
+ extend,
+ getWidthOfInput,
+ sortByAlpha,
+ sortByScore,
+}
+from './lib/utils.js';
import './lib/polyfills.js';
/**
* Choices
*/
-export default class Choices {
- constructor(element = '[data-choice]', userConfig = {}) {
- // If there are multiple elements, create a new instance
- // for each element besides the first one (as that already has an instance)
- if (isType('String', element)) {
- const elements = document.querySelectorAll(element);
- if (elements.length > 1) {
- for (let i = 1; i < elements.length; i++) {
- const el = elements[i];
- new Choices(el, userConfig);
- }
- }
+export
+default class Choices {
+ constructor(element = '[data-choice]', userConfig = {}) {
+ // If there are multiple elements, create a new instance
+ // for each element besides the first one (as that already has an instance)
+ if (isType('String', element)) {
+ const elements = document.querySelectorAll(element);
+ if (elements.length > 1) {
+ for (let i = 1; i < elements.length; i++) {
+ const el = elements[i];
+ new Choices(el, userConfig);
}
+ }
+ }
- const defaultConfig = {
- items: [],
- choices: [],
- maxItemCount: -1,
- addItems: true,
- removeItems: true,
- removeItemButton: false,
- editItems: false,
- duplicateItems: true,
- delimiter: ',',
- paste: true,
- search: true,
- flip: true,
- regexFilter: null,
- shouldSort: true,
- sortFilter: sortByAlpha,
- sortFields: ['label', 'value'],
- placeholder: true,
- placeholderValue: null,
- prependValue: null,
- appendValue: null,
- loadingText: 'Loading...',
- noResultsText: 'No results round',
- noChoicesText: 'No choices to choose from',
- classNames: {
- containerOuter: 'choices',
- containerInner: 'choices__inner',
- input: 'choices__input',
- inputCloned: 'choices__input--cloned',
- list: 'choices__list',
- listItems: 'choices__list--multiple',
- listSingle: 'choices__list--single',
- listDropdown: 'choices__list--dropdown',
- item: 'choices__item',
- itemSelectable: 'choices__item--selectable',
- itemDisabled: 'choices__item--disabled',
- itemChoice: 'choices__item--choice',
- placeholder: 'choices__placeholder',
- group: 'choices__group',
- groupHeading: 'choices__heading',
- button: 'choices__button',
- activeState: 'is-active',
- focusState: 'is-focused',
- openState: 'is-open',
- disabledState: 'is-disabled',
- highlightedState: 'is-highlighted',
- hiddenState: 'is-hidden',
- flippedState: 'is-flipped',
- loadingState: 'is-loading',
- },
- callbackOnInit: () => {},
- callbackOnAddItem: (id, value, passedInput) => {},
- callbackOnRemoveItem: (id, value, passedInput) => {},
- callbackOnHighlightItem: (id, value, passedInput) => {},
- callbackOnUnhighlightItem: (id, value, passedInput) => {},
- callbackOnChange: (value, passedInput) => {},
- };
+ const defaultConfig = {
+ items: [],
+ choices: [],
+ maxItemCount: -1,
+ addItems: true,
+ removeItems: true,
+ removeItemButton: false,
+ editItems: false,
+ duplicateItems: true,
+ delimiter: ',',
+ paste: true,
+ search: true,
+ flip: true,
+ regexFilter: null,
+ shouldSort: true,
+ sortFilter: sortByAlpha,
+ sortFields: ['label', 'value'],
+ placeholder: true,
+ placeholderValue: null,
+ prependValue: null,
+ appendValue: null,
+ loadingText: 'Loading...',
+ noResultsText: 'No results round',
+ noChoicesText: 'No choices to choose from',
+ classNames: {
+ containerOuter: 'choices',
+ containerInner: 'choices__inner',
+ input: 'choices__input',
+ inputCloned: 'choices__input--cloned',
+ list: 'choices__list',
+ listItems: 'choices__list--multiple',
+ listSingle: 'choices__list--single',
+ listDropdown: 'choices__list--dropdown',
+ item: 'choices__item',
+ itemSelectable: 'choices__item--selectable',
+ itemDisabled: 'choices__item--disabled',
+ itemChoice: 'choices__item--choice',
+ placeholder: 'choices__placeholder',
+ group: 'choices__group',
+ groupHeading: 'choices__heading',
+ button: 'choices__button',
+ activeState: 'is-active',
+ focusState: 'is-focused',
+ openState: 'is-open',
+ disabledState: 'is-disabled',
+ highlightedState: 'is-highlighted',
+ hiddenState: 'is-hidden',
+ flippedState: 'is-flipped',
+ loadingState: 'is-loading',
+ },
+ callbackOnInit: () => {},
+ callbackOnAddItem: (id, value, passedInput) => {},
+ callbackOnRemoveItem: (id, value, passedInput) => {},
+ callbackOnHighlightItem: (id, value, passedInput) => {},
+ callbackOnUnhighlightItem: (id, value, passedInput) => {},
+ callbackOnChange: (value, passedInput) => {},
+ };
- // Merge options with user options
- this.config = extend(defaultConfig, userConfig);
+ // Merge options with user options
+ this.config = extend(defaultConfig, userConfig);
- // Create data store
- this.store = new Store(this.render);
+ // Create data store
+ this.store = new Store(this.render);
- // State tracking
- this.initialised = false;
- this.currentState = {};
- this.prevState = {};
- this.currentValue = '';
+ // State tracking
+ this.initialised = false;
+ this.currentState = {};
+ this.prevState = {};
+ this.currentValue = '';
- // Retrieve triggering element (i.e. element with 'data-choice' trigger)
- this.passedElement = isType('String', element) ? document.querySelector(element) : element;
+ // Retrieve triggering element (i.e. element with 'data-choice' trigger)
+ this.passedElement = isType('String', element) ? document.querySelector(element) : element;
- if (!this.passedElement) {
- console.error('Passed element not found');
- return;
- }
+ if (!this.passedElement) {
+ console.error('Passed element not found');
+ return;
+ }
- this.highlightPosition = 0;
- this.canSearch = this.config.search;
+ this.highlightPosition = 0;
+ this.canSearch = this.config.search;
- // Assing preset choices from passed object
- this.presetChoices = this.config.choices;
+ // Assing preset choices from passed object
+ this.presetChoices = this.config.choices;
- // Assign preset items from passed object first
- this.presetItems = this.config.items;
+ // Assign preset items from passed object first
+ this.presetItems = this.config.items;
- // Then add any values passed from attribute
- if (this.passedElement.value) {
- this.presetItems = this.presetItems.concat(this.passedElement.value.split(this.config.delimiter));
- }
+ // Then add any values passed from attribute
+ if (this.passedElement.value) {
+ this.presetItems = this.presetItems.concat(this.passedElement.value.split(this.config.delimiter));
+ }
- // Bind methods
- this.init = this.init.bind(this);
- this.render = this.render.bind(this);
- this.destroy = this.destroy.bind(this);
- this.disable = this.disable.bind(this);
+ // Bind methods
+ this.init = this.init.bind(this);
+ this.render = this.render.bind(this);
+ this.destroy = this.destroy.bind(this);
+ this.disable = this.disable.bind(this);
- // Bind event handlers
- this._onFocus = this._onFocus.bind(this);
- this._onBlur = this._onBlur.bind(this);
- this._onKeyUp = this._onKeyUp.bind(this);
- this._onKeyDown = this._onKeyDown.bind(this);
- this._onClick = this._onClick.bind(this);
- this._onTouchMove = this._onTouchMove.bind(this);
- this._onTouchEnd = this._onTouchEnd.bind(this);
- this._onMouseDown = this._onMouseDown.bind(this);
- this._onMouseOver = this._onMouseOver.bind(this);
- this._onPaste = this._onPaste.bind(this);
- this._onInput = this._onInput.bind(this);
+ // Bind event handlers
+ this._onFocus = this._onFocus.bind(this);
+ this._onBlur = this._onBlur.bind(this);
+ this._onKeyUp = this._onKeyUp.bind(this);
+ this._onKeyDown = this._onKeyDown.bind(this);
+ this._onClick = this._onClick.bind(this);
+ this._onTouchMove = this._onTouchMove.bind(this);
+ this._onTouchEnd = this._onTouchEnd.bind(this);
+ this._onMouseDown = this._onMouseDown.bind(this);
+ this._onMouseOver = this._onMouseOver.bind(this);
+ this._onPaste = this._onPaste.bind(this);
+ this._onInput = this._onInput.bind(this);
- // Monitor touch taps/scrolls
- this.wasTap = true;
+ // Monitor touch taps/scrolls
+ this.wasTap = true;
- // Cutting the mustard
- const cuttingTheMustard = 'querySelector' in document && 'addEventListener' in document && 'classList' in document.createElement('div');
- if (!cuttingTheMustard) console.error('Choices: Your browser doesn\'t support Choices');
+ // Cutting the mustard
+ const cuttingTheMustard = 'querySelector' in document && 'addEventListener' in document && 'classList' in document.createElement('div');
+ if (!cuttingTheMustard) console.error('Choices: Your browser doesn\'t support Choices');
- // Input type check
- const canInit = this.passedElement && isElement(this.passedElement) && ['select-one', 'select-multiple', 'text'].some(type => type === this.passedElement.type);
+ // Input type check
+ const canInit = this.passedElement && isElement(this.passedElement) && ['select-one', 'select-multiple', 'text'].some(type => type === this.passedElement.type);
- if (canInit) {
- // If element has already been initalised with Choices
- if (this.passedElement.getAttribute('data-choice') === 'active') return;
+ if (canInit) {
+ // If element has already been initalised with Choices
+ if (this.passedElement.getAttribute('data-choice') === 'active') return;
- // Let's go
- this.init();
+ // Let's go
+ this.init();
+ } else {
+ console.error('Incompatible input passed');
+ }
+ }
+
+ /**
+ * Initialise Choices
+ * @return
+ * @public
+ */
+ init(callback = this.config.callbackOnInit) {
+ if (this.initialised === false) {
+ // Set initialise flag
+ this.initialised = true;
+
+ // Create required elements
+ this._createTemplates();
+
+ // Generate input markup
+ this._createInput();
+
+ this.store.subscribe(this.render);
+
+ // Render any items
+ this.render();
+
+ // Trigger event listeners
+ this._addEventListeners();
+
+ // Run callback if it is a function
+ if (callback) {
+ if (isType('Function', callback)) {
+ callback();
} else {
- console.error('Incompatible input passed');
+ console.error('callbackOnInit: Callback is not a function');
}
+ }
+ }
+ }
+
+ /**
+ * Destroy Choices and nullify values
+ * @return
+ * @public
+ */
+ destroy() {
+ if (this.initialised === true) {
+ this._removeEventListeners();
+
+ this.passedElement.classList.remove(this.config.classNames.input, this.config.classNames.hiddenState);
+ this.passedElement.tabIndex = '';
+ this.passedElement.removeAttribute('style', 'display:none;');
+ this.passedElement.removeAttribute('aria-hidden');
+
+ this.containerOuter.outerHTML = this.passedElement.outerHTML;
+
+ this.passedElement = null;
+ this.userConfig = null;
+ this.config = null;
+ this.store = null;
+
+ this.initialised = false;
+ }
+ }
+
+ /**
+ * Select item (a selected item can be deleted)
+ * @param {Element} item Element to select
+ * @return {Object} Class instance
+ * @public
+ */
+ highlightItem(item) {
+ if (!item) return;
+ const id = item.id;
+ this.store.dispatch(highlightItem(id, true));
+
+ // Run callback if it is a function
+ if (this.config.callbackOnHighlightItem) {
+ const callback = this.config.callbackOnHighlightItem;
+ if (isType('Function', callback)) {
+ callback(id, item.value, this.passedElement);
+ } else {
+ console.error('callbackOnHighlightItem: Callback is not a function');
+ }
}
- /**
- * Initialise Choices
- * @return
- * @public
- */
- init(callback = this.config.callbackOnInit) {
- if (this.initialised === false) {
- // Set initialise flag
- this.initialised = true;
+ return this;
+ }
- // Create required elements
- this._createTemplates();
+ /**
+ * Deselect item
+ * @param {Element} item Element to de-select
+ * @return {Object} Class instance
+ * @public
+ */
+ unhighlightItem(item) {
+ if (!item) return;
+ const id = item.id;
+ this.store.dispatch(highlightItem(id, false));
- // Generate input markup
- this._createInput();
-
- this.store.subscribe(this.render);
-
- // Render any items
- this.render();
-
- // Trigger event listeners
- this._addEventListeners();
-
- // Run callback if it is a function
- if (callback) {
- if (isType('Function', callback)) {
- callback();
- } else {
- console.error('callbackOnInit: Callback is not a function');
- }
- }
- }
+ // Run callback if it is a function
+ if (this.config.callbackOnUnhighlightItem) {
+ const callback = this.config.callbackOnUnhighlightItem;
+ if (isType('Function', callback)) {
+ callback(id, item.value, this.passedElement);
+ } else {
+ console.error('callbackOnUnhighlightItem: Callback is not a function');
+ }
}
- /**
- * Destroy Choices and nullify values
- * @return
- * @public
- */
- destroy() {
- if (this.initialised === true) {
- this._removeEventListeners();
+ return this;
+ }
- this.passedElement.classList.remove(this.config.classNames.input, this.config.classNames.hiddenState);
- this.passedElement.tabIndex = '';
- this.passedElement.removeAttribute('style', 'display:none;');
- this.passedElement.removeAttribute('aria-hidden');
+ /**
+ * Highlight items within store
+ * @return {Object} Class instance
+ * @public
+ */
+ highlightAll() {
+ const items = this.store.getItems();
+ items.forEach((item) => {
+ this.highlightItem(item);
+ });
- this.containerOuter.outerHTML = this.passedElement.outerHTML;
+ return this;
+ }
- this.passedElement = null;
- this.userConfig = null;
- this.config = null;
- this.store = null;
+ /**
+ * Deselect items within store
+ * @return {Object} Class instance
+ * @public
+ */
+ unhighlightAll() {
+ const items = this.store.getItems();
+ items.forEach((item) => {
+ this.unhighlightItem(item);
+ });
- this.initialised = false;
- }
+ return this;
+ }
+
+ /**
+ * Remove an item from the store by its value
+ * @param {String} value Value to search for
+ * @return {Object} Class instance
+ * @public
+ */
+ removeItemsByValue(value) {
+ if (!value || !isType('String', value)) {
+ console.error('removeItemsByValue: No value was passed to be removed');
+ return;
}
- /**
- * Select item (a selected item can be deleted)
- * @param {Element} item Element to select
- * @return {Object} Class instance
- * @public
- */
- highlightItem(item) {
- if (!item) return;
- const id = item.id;
- this.store.dispatch(highlightItem(id, true));
+ const items = this.store.getItemsFilteredByActive();
- // Run callback if it is a function
- if (this.config.callbackOnHighlightItem) {
- const callback = this.config.callbackOnHighlightItem;
- if (isType('Function', callback)) {
- callback(id, item.value, this.passedElement);
- } else {
- console.error('callbackOnHighlightItem: Callback is not a function');
- }
- }
+ items.forEach((item) => {
+ if (item.value === value) {
+ this._removeItem(item);
+ }
+ });
- return this;
+ return this;
+ }
+
+ /**
+ * Remove all items from store array
+ * @note Removed items are soft deleted
+ * @param {Number} excludedId Optionally exclude item by ID
+ * @return {Object} Class instance
+ * @public
+ */
+ removeActiveItems(excludedId) {
+ const items = this.store.getItemsFilteredByActive();
+
+ items.forEach((item) => {
+ if (item.active && excludedId !== item.id) {
+ this._removeItem(item);
+ }
+ });
+
+ return this;
+ }
+
+ /**
+ * Remove all selected items from store
+ * @note Removed items are soft deleted
+ * @return {Object} Class instance
+ * @public
+ */
+ removeHighlightedItems() {
+ const items = this.store.getItemsFilteredByActive();
+
+ items.forEach((item) => {
+ if (item.highlighted && item.active) {
+ this._removeItem(item);
+ }
+ });
+
+ return this;
+ }
+
+ /**
+ * Show dropdown to user by adding active state class
+ * @return {Object} Class instance
+ * @public
+ */
+ showDropdown(focusInput = false) {
+ const body = document.body;
+ const html = document.documentElement;
+ const winHeight = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight);
+
+ this.containerOuter.classList.add(this.config.classNames.openState);
+ this.containerOuter.setAttribute('aria-expanded', 'true');
+ this.dropdown.classList.add(this.config.classNames.activeState);
+
+ const dimensions = this.dropdown.getBoundingClientRect();
+ const dropdownPos = Math.ceil(dimensions.top + window.scrollY + dimensions.height);
+ // If flip is enabled and the dropdown bottom position is greater than the window height flip the dropdown.
+ const shouldFlip = this.config.flip ? dropdownPos >= winHeight : false;
+
+ if (shouldFlip) {
+ this.containerOuter.classList.add(this.config.classNames.flippedState);
+ } else {
+ this.containerOuter.classList.remove(this.config.classNames.flippedState);
}
- /**
- * Deselect item
- * @param {Element} item Element to de-select
- * @return {Object} Class instance
- * @public
- */
- unhighlightItem(item) {
- if (!item) return;
- const id = item.id;
- this.store.dispatch(highlightItem(id, false));
-
- // Run callback if it is a function
- if (this.config.callbackOnUnhighlightItem) {
- const callback = this.config.callbackOnUnhighlightItem;
- if (isType('Function', callback)) {
- callback(id, item.value, this.passedElement);
- } else {
- console.error('callbackOnUnhighlightItem: Callback is not a function');
- }
- }
-
- return this;
+ // Optionally focus the input if we have a search input
+ if (focusInput && this.canSearch && document.activeElement !== this.input) {
+ this.input.focus();
}
- /**
- * Highlight items within store
- * @return {Object} Class instance
- * @public
- */
- highlightAll() {
- const items = this.store.getItems();
- items.forEach((item) => {
- this.highlightItem(item);
+ return this;
+ }
+
+ /**
+ * Hide dropdown from user
+ * @return {Object} Class instance
+ * @public
+ */
+ hideDropdown(blurInput = false) {
+ // A dropdown flips if it does not have space within the page
+ const isFlipped = this.containerOuter.classList.contains(this.config.classNames.flippedState);
+
+ this.containerOuter.classList.remove(this.config.classNames.openState);
+ this.containerOuter.setAttribute('aria-expanded', 'false');
+
+ this.dropdown.classList.remove(this.config.classNames.activeState);
+
+ if (isFlipped) {
+ this.containerOuter.classList.remove(this.config.classNames.flippedState);
+ }
+
+ // Optionally blur the input if we have a search input
+ if (blurInput && this.canSearch && document.activeElement === this.input) {
+ this.input.blur();
+ }
+
+ return this;
+ }
+
+ /**
+ * Determine whether to hide or show dropdown based on its current state
+ * @return {Object} Class instance
+ * @public
+ */
+ toggleDropdown() {
+ const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
+ if (hasActiveDropdown) {
+ this.hideDropdown();
+ } else {
+ this.showDropdown(true);
+ }
+
+ return this;
+ }
+
+ /**
+ * Get value(s) of input (i.e. inputted items (text) or selected choices (select))
+ * @param {Boolean} valueOnly Get only values of selected items, otherwise return selected items
+ * @return {Array/String} selected value (select-one) or array of selected items (inputs & select-multiple)
+ * @public
+ */
+ getValue(valueOnly = false) {
+ const items = this.store.getItemsFilteredByActive();
+ const selectedItems = [];
+
+ items.forEach((item) => {
+ if (this.passedElement.type === 'text') {
+ selectedItems.push(valueOnly ? item.value : item);
+ } else if (item.active) {
+ selectedItems.push(valueOnly ? item.value : item);
+ }
+ });
+
+ if (this.passedElement.type === 'select-one') {
+ return selectedItems[0];
+ }
+
+ return selectedItems;
+ }
+
+ /**
+ * Set value of input. If the input is a select box, a choice will be created and selected otherwise
+ * an item will created directly.
+ * @param {Array} args Array of value objects or value strings
+ * @return {Object} Class instance
+ * @public
+ */
+ setValue(args) {
+ if (this.initialised === true) {
+ // Convert args to an itterable array
+ const values = [...args];
+
+ values.forEach((item) => {
+ if (isType('Object', item)) {
+ if (!item.value) return;
+ // If we are dealing with a select input, we need to create an option first
+ // that is then selected. For text inputs we can just add items normally.
+ if (this.passedElement.type !== 'text') {
+ this._addChoice(true, false, item.value, item.label, -1);
+ } else {
+ this._addItem(item.value, item.label, item.id);
+ }
+ } else if (isType('String', item)) {
+ if (this.passedElement.type !== 'text') {
+ this._addChoice(true, false, item, item, -1);
+ } else {
+ this._addItem(item);
+ }
+ }
+ });
+ }
+
+ return this;
+ }
+
+ /**
+ * Select value of select box via the value of an existing choice
+ * @param {Array/String} value An array of strings of a single string
+ * @return {Object} Class instance
+ * @public
+ */
+ setValueByChoice(value) {
+ if (this.passedElement.type !== 'text') {
+ const choices = this.store.getChoices();
+ // If only one value has been passed, convert to array
+ const choiceValue = isType('Array', value) ? value : [value];
+
+ // Loop through each value and
+ choiceValue.forEach((val) => {
+ const foundChoice = choices.find((choice) => {
+ // Check 'value' property exists and the choice isn't already selected
+ return choice.value === val;
});
- return this;
- }
-
- /**
- * Deselect items within store
- * @return {Object} Class instance
- * @public
- */
- unhighlightAll() {
- const items = this.store.getItems();
- items.forEach((item) => {
- this.unhighlightItem(item);
- });
-
- return this;
- }
-
- /**
- * Remove an item from the store by its value
- * @param {String} value Value to search for
- * @return {Object} Class instance
- * @public
- */
- removeItemsByValue(value) {
- if (!value || !isType('String', value)) {
- console.error('removeItemsByValue: No value was passed to be removed');
- return;
- }
-
- const items = this.store.getItemsFilteredByActive();
-
- items.forEach((item) => {
- if (item.value === value) {
- this._removeItem(item);
- }
- });
-
- return this;
- }
-
- /**
- * Remove all items from store array
- * @note Removed items are soft deleted
- * @param {Number} excludedId Optionally exclude item by ID
- * @return {Object} Class instance
- * @public
- */
- removeActiveItems(excludedId) {
- const items = this.store.getItemsFilteredByActive();
-
- items.forEach((item) => {
- if (item.active && excludedId !== item.id) {
- this._removeItem(item);
- }
- });
-
- return this;
- }
-
- /**
- * Remove all selected items from store
- * @note Removed items are soft deleted
- * @return {Object} Class instance
- * @public
- */
- removeHighlightedItems() {
- const items = this.store.getItemsFilteredByActive();
-
- items.forEach((item) => {
- if (item.highlighted && item.active) {
- this._removeItem(item);
- }
- });
-
- return this;
- }
-
- /**
- * Show dropdown to user by adding active state class
- * @return {Object} Class instance
- * @public
- */
- showDropdown(focusInput = false) {
- const body = document.body;
- const html = document.documentElement;
- const winHeight = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight);
-
- this.containerOuter.classList.add(this.config.classNames.openState);
- this.containerOuter.setAttribute('aria-expanded', 'true');
- this.dropdown.classList.add(this.config.classNames.activeState);
-
- const dimensions = this.dropdown.getBoundingClientRect();
- const dropdownPos = Math.ceil(dimensions.top + window.scrollY + dimensions.height);
- // If flip is enabled and the dropdown bottom position is greater than the window height flip the dropdown.
- const shouldFlip = this.config.flip ? dropdownPos >= winHeight : false;
-
- if (shouldFlip) {
- this.containerOuter.classList.add(this.config.classNames.flippedState);
+ if (foundChoice) {
+ if (!foundChoice.selected) {
+ this._addItem(foundChoice.value, foundChoice.label, foundChoice.id);
+ } else {
+ console.warn('Attempting to select choice already selected');
+ }
} else {
- this.containerOuter.classList.remove(this.config.classNames.flippedState);
+ console.warn('Attempting to select choice that does not exist');
}
-
- // Optionally focus the input if we have a search input
- if (focusInput && this.canSearch && document.activeElement !== this.input) {
- this.input.focus();
- }
-
- return this;
+ });
}
-
- /**
- * Hide dropdown from user
- * @return {Object} Class instance
- * @public
- */
- hideDropdown(blurInput = false) {
- // A dropdown flips if it does not have space within the page
- const isFlipped = this.containerOuter.classList.contains(this.config.classNames.flippedState);
-
- this.containerOuter.classList.remove(this.config.classNames.openState);
- this.containerOuter.setAttribute('aria-expanded', 'false');
-
- this.dropdown.classList.remove(this.config.classNames.activeState);
-
- if (isFlipped) {
- this.containerOuter.classList.remove(this.config.classNames.flippedState);
- }
-
- // Optionally blur the input if we have a search input
- if (blurInput && this.canSearch && document.activeElement === this.input) {
- this.input.blur();
- }
-
- return this;
- }
-
- /**
- * Determine whether to hide or show dropdown based on its current state
- * @return {Object} Class instance
- * @public
- */
- toggleDropdown() {
- const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
- if (hasActiveDropdown) {
- this.hideDropdown();
- } else {
- this.showDropdown(true);
- }
-
- return this;
- }
-
- /**
- * Get value(s) of input (i.e. inputted items (text) or selected choices (select))
- * @param {Boolean} valueOnly Get only values of selected items, otherwise return selected items
- * @return {Array/String} selected value (select-one) or array of selected items (inputs & select-multiple)
- * @public
- */
- getValue(valueOnly = false) {
- const items = this.store.getItemsFilteredByActive();
- const selectedItems = [];
-
- items.forEach((item) => {
- if (this.passedElement.type === 'text') {
- selectedItems.push(valueOnly ? item.value : item);
- } else if (item.active) {
- selectedItems.push(valueOnly ? item.value : item);
- }
- });
-
- if (this.passedElement.type === 'select-one') {
- return selectedItems[0];
- }
-
- return selectedItems;
- }
-
- /**
- * Set value of input. If the input is a select box, a choice will be created and selected otherwise
- * an item will created directly.
- * @param {Array} args Array of value objects or value strings
- * @return {Object} Class instance
- * @public
- */
- setValue(args) {
- if (this.initialised === true) {
- // Convert args to an itterable array
- const values = [...args];
-
- values.forEach((item) => {
- if (isType('Object', item)) {
- if (!item.value) return;
- // If we are dealing with a select input, we need to create an option first
- // that is then selected. For text inputs we can just add items normally.
- if (this.passedElement.type !== 'text') {
- this._addChoice(true, false, item.value, item.label, -1);
- } else {
- this._addItem(item.value, item.label, item.id);
- }
- } else if (isType('String', item)) {
- if (this.passedElement.type !== 'text') {
- this._addChoice(true, false, item, item, -1);
- } else {
- this._addItem(item);
- }
- }
- });
- }
-
- return this;
- }
-
- /**
- * Select value of select box via the value of an existing choice
- * @param {Array/String} value An array of strings of a single string
- * @return {Object} Class instance
- * @public
- */
- setValueByChoice(value) {
- if (this.passedElement.type !== 'text') {
- const choices = this.store.getChoices();
- // If only one value has been passed, convert to array
- const choiceValue = isType('Array', value) ? value : [value];
-
- // Loop through each value and
- choiceValue.forEach((val) => {
- const foundChoice = choices.find((choice) => {
- // Check 'value' property exists and the choice isn't already selected
- return choice.value === val;
- });
-
- if (foundChoice) {
- if (!foundChoice.selected) {
- this._addItem(foundChoice.value, foundChoice.label, foundChoice.id);
- } else {
- console.warn('Attempting to select choice already selected');
- }
- } else {
- console.warn('Attempting to select choice that does not exist');
- }
- });
- }
- return this;
- }
-
- /**
- * Direct populate choices
- * @param {Array} choices - Choices to insert
- * @param {String} value - Name of 'value' property
- * @param {String} label - Name of 'label' property
- * @return {Object} Class instance
- * @public
- */
- setChoices(choices, value, label) {
- if (this.initialised === true) {
- if (this.passedElement.type === 'select-one' || this.passedElement.type === 'select-multiple') {
- if (!isType('Array', choices) || !value) return;
-
- if (choices && choices.length) {
- this.containerOuter.classList.remove(this.config.classNames.loadingState);
- choices.forEach((result, index) => {
- if (result.choices) {
- this._addGroup(result, index);
- } else {
- this._addChoice(result.selected ? result.selected : false, result.disabled ? result.disabled : false, result[value], result[label]);
- }
- });
- }
- }
- }
- return this;
- }
-
- /**
- * Clear items,choices and groups
- * @note Hard delete
- * @return {Object} Class instance
- * @public
- */
- clearStore() {
- this.store.dispatch(clearAll());
- return this;
- }
-
- /**
- * Set value of input to blank
- * @return {Object} Class instance
- * @public
- */
- clearInput() {
- if (this.input.value) this.input.value = '';
- if (this.passedElement.type !== 'select-one') {
- this.input.style.width = getWidthOfInput(this.input);
- }
- if (this.passedElement.type !== 'text' && this.config.search) {
- this.isSearching = false;
- this.store.dispatch(activateChoices(true));
- }
- return this;
- }
-
- /**
- * Disable interaction with Choices
- * @return {Object} Class instance
- * @public
- */
- disable() {
- this.passedElement.disabled = true;
- if (this.initialised) {
- if (!this.containerOuter.classList.contains(this.config.classNames.disabledState)) {
- this._removeEventListeners();
- this.passedElement.setAttribute('disabled', '');
- this.input.setAttribute('disabled', '');
- this.containerOuter.classList.add(this.config.classNames.disabledState);
- this.containerOuter.setAttribute('aria-disabled', 'true');
- }
- }
- return this;
- }
-
- /**
- * Enable interaction with Choices
- * @return {Object} Class instance
- */
- enable() {
- this.passedElement.disabled = false;
- if (this.initialised) {
- if (this.containerOuter.classList.contains(this.config.classNames.disabledState)) {
- this._addEventListeners();
- this.passedElement.removeAttribute('disabled');
- this.input.removeAttribute('disabled');
- this.containerOuter.classList.remove(this.config.classNames.disabledState);
- this.containerOuter.removeAttribute('aria-disabled');
- }
- }
- return this;
- }
-
- /**
- * Populate options via ajax callback
- * @param {Function} fn Passed
- * @return {Object} Class instance
- * @public
- */
- ajax(fn) {
- if (this.initialised === true) {
- if (this.passedElement.type === 'select-one' || this.passedElement.type === 'select-multiple') {
- this.containerOuter.classList.add(this.config.classNames.loadingState);
- this.containerOuter.setAttribute('aria-busy', 'true');
- if (this.passedElement.type === 'select-one') {
- const placeholderItem = this._getTemplate('placeholder', this.config.loadingText);
- this.itemList.appendChild(placeholderItem);
- } else {
- this.input.placeholder = this.config.loadingText;
- }
-
- const callback = (results, value, label) => {
- if (!isType('Array', results) || !value) return;
- if (results && results.length) {
- // Remove loading states/text
- this.containerOuter.classList.remove(this.config.classNames.loadingState);
- if (this.passedElement.type === 'select-multiple') {
- const placeholder = this.config.placeholder ? this.config.placeholderValue || this.passedElement.getAttribute('placeholder') : false;
- if (placeholder) {
- this.input.placeholder = placeholder;
- }
- }
-
- // Add each result as a choice
- results.forEach((result, index) => {
- this._addChoice(false, false, result[value], result[label]);
- });
- }
- this.containerOuter.removeAttribute('aria-busy');
- };
- fn(callback);
- }
- }
- return this;
- }
-
- /**
- * Call change callback
- * @param {String} value - last added/deleted/selected value
- * @return
- * @private
- */
- _triggerChange(value) {
- if (!value) return;
-
- // Run callback if it is a function
- if (this.config.callbackOnChange) {
- const callback = this.config.callbackOnChange;
- if (isType('Function', callback)) {
- callback(value, this.passedElement);
- } else {
- console.error('callbackOnChange: Callback is not a function');
- }
- }
- }
-
-
- /**
- * Process enter/click of an item button
- * @param {Array} activeItems The currently active items
- * @param {Element} element Button being interacted with
- * @return
- * @private
- */
- _handleButtonAction(activeItems, element) {
- if (!activeItems || !element) return;
-
- // If we are clicking on a button
- if (this.config.removeItems && this.config.removeItemButton) {
- const itemId = element.parentNode.getAttribute('data-id');
- const itemToRemove = activeItems.find((item) => item.id === parseInt(itemId, 10));
-
- // Remove item associated with button
- this._removeItem(itemToRemove);
- this._triggerChange(itemToRemove.value);
-
- if (this.passedElement.type === 'select-one') {
- const placeholder = this.config.placeholder ? this.config.placeholderValue || this.passedElement.getAttribute('placeholder') : false;
- if (placeholder) {
- const placeholderItem = this._getTemplate('placeholder', placeholder);
- this.itemList.appendChild(placeholderItem);
- }
- }
- }
- }
-
- /**
- * Process click of an item
- * @param {Array} activeItems The currently active items
- * @param {Element} element Item being interacted with
- * @param {Boolean} hasShiftKey Whether the user has the shift key active
- * @return
- * @private
- */
- _handleItemAction(activeItems, element, hasShiftKey = false) {
- if (!activeItems || !element) return;
-
- // If we are clicking on an item
- if (this.config.removeItems && this.passedElement.type !== 'select-one') {
- const passedId = element.getAttribute('data-id');
-
- // We only want to select one item with a click
- // so we deselect any items that aren't the target
- // unless shift is being pressed
- activeItems.forEach((item) => {
- if (item.id === parseInt(passedId, 10) && !item.highlighted) {
- this.highlightItem(item);
- } else if (!hasShiftKey) {
- if (item.highlighted) {
- this.unhighlightItem(item);
- }
- }
- });
-
- // Focus input as without focus, a user cannot do anything with a
- // highlighted item
- if (document.activeElement !== this.input) this.input.focus();
- }
- }
-
- /**
- * Process click of a choice
- * @param {Array} activeItems The currently active items
- * @param {Element} element Choice being interacted with
- * @return {[type]} [description]
- */
- _handleChoiceAction(activeItems, element) {
- if (!activeItems || !element) return;
-
- // If we are clicking on an option
- const id = element.getAttribute('data-id');
- const choice = this.store.getChoiceById(id);
- const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
-
- if (choice && !choice.selected && !choice.disabled) {
- const canAddItem = this._canAddItem(activeItems, choice.value);
-
- if (canAddItem.response) {
- this._addItem(choice.value, choice.label, choice.id);
- this._triggerChange(choice.value);
- }
- }
-
- this.clearInput(this.passedElement);
-
- // We wont to close the dropdown if we are dealing with a single select box
- if (hasActiveDropdown && this.passedElement.type === 'select-one') {
- this.hideDropdown();
- this.containerOuter.focus();
- }
- }
-
- /**
- * Process back space event
- * @param {Array} Active items
- * @return
- * @private
- */
- _handleBackspace(activeItems) {
- if (this.config.removeItems && activeItems) {
- const lastItem = activeItems[activeItems.length - 1];
- const hasHighlightedItems = activeItems.some((item) => item.highlighted === true);
-
- // If editing the last item is allowed and there are not other selected items,
- // we can edit the item value. Otherwise if we can remove items, remove all selected items
- if (this.config.editItems && !hasHighlightedItems && lastItem) {
- this.input.value = lastItem.value;
- this._removeItem(lastItem);
- this._triggerChange(lastItem.value);
- } else {
- if (!hasHighlightedItems) { this.highlightItem(lastItem); }
- this.removeHighlightedItems();
- }
- }
- }
-
- /**
- * Validates whether an item can be added by a user
- * @param {Array} activeItems The currently active items
- * @param {String} value Value of item to add
- * @return {Object} Response: Whether user can add item
- * Notice: Notice show in dropdown
- */
- _canAddItem(activeItems, value) {
- let canAddItem = true;
- let notice = `Press Enter to add "${value}"`;
-
- if (this.passedElement.type === 'select-multiple' || this.passedElement.type === 'text') {
- if (this.config.maxItemCount > 0 && this.config.maxItemCount <= this.itemList.children.length) {
- // If there is a max entry limit and we have reached that limit
- // don't update
- canAddItem = false;
- notice = `Only ${this.config.maxItemCount} values can be added.`;
- }
- }
-
- if (this.passedElement.type === 'text' && this.config.addItems) {
- const isUnique = !activeItems.some((item) => item.value === value.trim());
-
- // If a user has supplied a regular expression filter
- if (this.config.regexFilter) {
- // Determine whether we can update based on whether
- // our regular expression passes
- canAddItem = this._regexFilter(value);
- }
-
- // If no duplicates are allowed, and the value already exists
- // in the array
- if (this.config.duplicateItems === false && !isUnique) {
- canAddItem = false;
- notice = 'Only unique values can be added.';
- }
- }
-
- return {
- response: canAddItem,
- notice,
- };
- }
-
- /**
- * Filter choices based on search value
- * @param {String} value Value to filter by
- * @return
- * @private
- */
- _searchChoices(value) {
- if (!value) return;
- if (this.input === document.activeElement) {
- const choices = this.store.getChoices();
- const hasUnactiveChoices = choices.some((option) => option.active !== true);
-
- // Check that we have a value to search and the input was an alphanumeric character
- if (value && value.length > 1) {
- const handleFilter = () => {
- const newValue = isType('String', value) ? value.trim() : value;
- const currentValue = isType('String', this.currentValue) ? this.currentValue.trim() : this.currentValue;
-
- if (newValue.length >= 1 && newValue !== `${currentValue} `) {
- const haystack = this.store.getChoicesFilteredBySelectable();
- const needle = newValue;
- const keys = isType('Array', this.config.sortFields) ? this.config.sortFields : [this.config.sortFields];
- const fuse = new Fuse(haystack, {
- keys,
- shouldSort: true,
- include: 'score',
- });
- const results = fuse.search(needle);
-
- this.currentValue = newValue;
- this.highlightPosition = 0;
- this.isSearching = true;
- this.store.dispatch(filterChoices(results));
- }
- };
-
- handleFilter();
- } else if (hasUnactiveChoices) {
- // Otherwise reset choices to active
- this.isSearching = false;
- this.store.dispatch(activateChoices(true));
- }
- }
- }
-
- /**
- * Trigger event listeners
- * @return
- * @private
- */
- _addEventListeners() {
- document.addEventListener('keyup', this._onKeyUp);
- document.addEventListener('keydown', this._onKeyDown);
- document.addEventListener('click', this._onClick);
- document.addEventListener('touchmove', this._onTouchMove);
- document.addEventListener('touchend', this._onTouchEnd);
- document.addEventListener('mousedown', this._onMouseDown);
- document.addEventListener('mouseover', this._onMouseOver);
-
- if (this.passedElement.type && this.passedElement.type === 'select-one') {
- this.containerOuter.addEventListener('focus', this._onFocus);
- this.containerOuter.addEventListener('blur', this._onBlur);
- }
-
- this.input.addEventListener('input', this._onInput);
- this.input.addEventListener('paste', this._onPaste);
- this.input.addEventListener('focus', this._onFocus);
- this.input.addEventListener('blur', this._onBlur);
- }
-
- /**
- * Destroy event listeners
- * @return
- * @private
- */
- _removeEventListeners() {
- document.removeEventListener('keyup', this._onKeyUp);
- document.removeEventListener('keydown', this._onKeyDown);
- document.removeEventListener('click', this._onClick);
- document.removeEventListener('touchmove', this._onTouchMove);
- document.removeEventListener('touchend', this._onTouchEnd);
- document.removeEventListener('mousedown', this._onMouseDown);
- document.removeEventListener('mouseover', this._onMouseOver);
-
- if (this.passedElement.type && this.passedElement.type === 'select-one') {
- this.containerOuter.removeEventListener('focus', this._onFocus);
- this.containerOuter.removeEventListener('blur', this._onBlur);
- }
-
- this.input.removeEventListener('input', this._onInput);
- this.input.removeEventListener('paste', this._onPaste);
- this.input.removeEventListener('focus', this._onFocus);
- this.input.removeEventListener('blur', this._onBlur);
- }
-
- /**
- * Key down event
- * @param {Object} e Event
- * @return
- */
- _onKeyDown(e) {
- if (e.target !== this.input && !this.containerOuter.contains(e.target)) return;
-
- const target = e.target;
-
- const activeItems = this.store.getItemsFilteredByActive();
- const hasFocusedInput = this.input === document.activeElement;
- const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
- const hasItems = this.itemList && this.itemList.children;
- const keyString = String.fromCharCode(e.keyCode);
-
- const backKey = 46;
- const deleteKey = 8;
- const enterKey = 13;
- const aKey = 65;
- const escapeKey = 27;
- const upKey = 38;
- const downKey = 40;
- const ctrlDownKey = e.ctrlKey || e.metaKey;
-
- // If a user is typing and the dropdown is not active
- if (this.passedElement.type !== 'text' && /[a-zA-Z0-9-_ ]/.test(keyString) && !hasActiveDropdown) {
- this.showDropdown(true);
- }
-
- this.canSearch = this.config.search;
-
- const onAKey = () => {
- // If CTRL + A or CMD + A have been pressed and there are items to select
- if (ctrlDownKey && hasItems) {
- this.canSearch = false;
- if (this.config.removeItems && !this.input.value && this.input === document.activeElement) {
- // Highlight items
- this.highlightAll(this.itemList.children);
- }
- }
- };
-
- const onEnterKey = () => {
- // If enter key is pressed and the input has a value
- if (this.passedElement.type === 'text' && target.value) {
- const value = this.input.value;
- const canAddItem = this._canAddItem(activeItems, value);
-
- // All is good, add
- if (canAddItem.response) {
- if (hasActiveDropdown) {
- this.hideDropdown();
- }
- this._addItem(value);
- this._triggerChange(value);
- this.clearInput(this.passedElement);
- }
- }
-
- if (target.hasAttribute('data-button')) {
- this._handleButtonAction(activeItems, target);
- e.preventDefault();
- }
-
- if (hasActiveDropdown) {
- const highlighted = this.dropdown.querySelector(`.${this.config.classNames.highlightedState}`);
-
- // If we have a highlighted choice
- if (highlighted) {
- this._handleChoiceAction(activeItems, highlighted);
- }
- } else if (this.passedElement.type === 'select-one') {
- // Open single select dropdown if it's not active
- if (!hasActiveDropdown) {
- this.showDropdown(true);
- e.preventDefault();
- }
- }
- };
-
- const onEscapeKey = () => {
- if (hasActiveDropdown) {
- this.toggleDropdown();
- }
- };
-
- const onDirectionKey = () => {
- // If up or down key is pressed, traverse through options
- if (hasActiveDropdown || this.passedElement.type === 'select-one') {
- // Show dropdown if focus
- if (!hasActiveDropdown) {
- this.showDropdown(true);
- }
-
- const currentEl = this.dropdown.querySelector(`.${this.config.classNames.highlightedState}`);
- const directionInt = e.keyCode === downKey ? 1 : -1;
- let nextEl;
-
- this.canSearch = false;
-
- if (currentEl) {
- nextEl = getAdjacentEl(currentEl, '[data-choice-selectable]', directionInt);
- } else {
- nextEl = this.dropdown.querySelector('[data-choice-selectable]');
- }
-
- if (nextEl) {
- // We prevent default to stop the cursor moving
- // when pressing the arrow
- if (!isScrolledIntoView(nextEl, this.choiceList, directionInt)) {
- this._scrollToChoice(nextEl, directionInt);
- }
- this._highlightChoice(nextEl);
- }
-
- // Prevent default to maintain cursor position whilst
- // traversing dropdown options
- e.preventDefault();
- }
- };
-
- const onDeleteKey = () => {
- // If backspace or delete key is pressed and the input has no value
- if (hasFocusedInput && !e.target.value && this.passedElement.type !== 'select-one') {
- this._handleBackspace(activeItems);
- e.preventDefault();
- }
- };
-
- // Map keys to key actions
- const keyDownActions = {
- [aKey]: onAKey,
- [enterKey]: onEnterKey,
- [escapeKey]: onEscapeKey,
- [upKey]: onDirectionKey,
- [downKey]: onDirectionKey,
- [deleteKey]: onDeleteKey,
- [backKey]: onDeleteKey,
- };
-
- // If keycode has a function, run it
- if (keyDownActions[e.keyCode]) {
- keyDownActions[e.keyCode]();
- }
- }
-
- /**
- * Key up event
- * @param {Object} e Event
- * @return
- * @private
- */
- _onKeyUp(e) {
- if (e.target !== this.input) return;
-
- // We are typing into a text input and have a value, we want to show a dropdown
- // notice. Otherwise hide the dropdown
- if (this.passedElement.type === 'text') {
- const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
- const value = this.input.value;
-
- if (value) {
- const activeItems = this.store.getItemsFilteredByActive();
- const canAddItem = this._canAddItem(activeItems, value);
-
- if (canAddItem.notice) {
- const dropdownItem = this._getTemplate('notice', canAddItem.notice);
- this.dropdown.innerHTML = dropdownItem.outerHTML;
- }
-
- if (canAddItem.response === true) {
- if (!hasActiveDropdown) {
- this.showDropdown();
- }
- } else if (!canAddItem.notice && hasActiveDropdown) {
- this.hideDropdown();
- }
- } else if (hasActiveDropdown) {
- this.hideDropdown();
- }
- } else {
- const backKey = 46;
- const deleteKey = 8;
-
- // If user has removed value...
- if ((e.keyCode === backKey || e.keyCode === deleteKey) && !e.target.value) {
- // ...and it is a multiple select input, activate choices (if searching)
- if (this.passedElement.type !== 'text' && this.isSearching) {
- this.isSearching = false;
- this.store.dispatch(activateChoices(true));
- }
- } else if (this.canSearch) {
- this._searchChoices(this.input.value);
- }
- }
- }
-
- /**
- * Input event
- * @param {Object} e Event
- * @return
- * @private
- */
- _onInput() {
- if (this.passedElement.type !== 'select-one') {
- if (this.config.placeholder && (this.config.placeholderValue || this.passedElement.getAttribute('placeholder'))) {
- // If there is a placeholder, we only want to set the width of the input when it is a greater
- // length than 75% of the placeholder. This stops the input jumping around.
- const placeholder = this.config.placeholder ? this.config.placeholderValue || this.passedElement.getAttribute('placeholder') : false;
- if (this.input.value && this.input.value.length >= (placeholder.length / 1.25)) {
- this.input.style.width = getWidthOfInput(this.input);
- }
- } else {
- // If there is no placeholder, resize input to contents
- this.input.style.width = getWidthOfInput(this.input);
- }
- }
- }
-
- /**
- * Touch move event
- * @param {Object} e Event
- * @return
- * @private
- */
- _onTouchMove() {
- if (this.wasTap === true) {
- this.wasTap = false;
- }
- }
-
- /**
- * Touch end event
- * @param {Object} e Event
- * @return
- * @private
- */
- _onTouchEnd(e) {
- const target = e.target || e.touches[0].target;
- const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
-
- // If a user tapped within our container...
- if (this.wasTap === true && this.containerOuter.contains(target)) {
- // ...and we aren't dealing with a single select box, show dropdown/focus input
- if ((target === this.containerOuter || target === this.containerInner) && this.passedElement.type !== 'select-one') {
- if (this.passedElement.type === 'text') {
- // If text element, we only want to focus the input (if it isn't already)
- if (document.activeElement !== this.input) {
- this.input.focus();
- }
- } else {
- if (!hasActiveDropdown) {
- // If a select box, we want to show the dropdown
- this.showDropdown(true);
- }
- }
- }
- // Prevents focus event firing
- e.stopPropagation();
- }
-
- this.wasTap = true;
- }
-
- /**
- * Mouse down event
- * @param {Object} e Event
- * @return
- * @private
- */
- _onMouseDown(e) {
- const target = e.target;
-
- if (this.containerOuter.contains(target) && target !== this.input) {
- const activeItems = this.store.getItemsFilteredByActive();
- const hasShiftKey = e.shiftKey;
-
- if (target.hasAttribute('data-item')) {
- this._handleItemAction(activeItems, target, hasShiftKey);
- } else if (target.hasAttribute('data-choice')) {
- this._handleChoiceAction(activeItems, target);
- }
-
- e.preventDefault();
- }
- }
-
- /**
- * Click event
- * @param {Object} e Event
- * @return
- * @private
- */
- _onClick(e) {
- const target = e.target;
- const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
- const activeItems = this.store.getItemsFilteredByActive();
-
- // If target is something that concerns us
- if (this.containerOuter.contains(target)) {
- // Handle button delete
- if (target.hasAttribute('data-button')) {
- this._handleButtonAction(activeItems, target);
- }
-
- if (!hasActiveDropdown) {
- if (this.passedElement.type === 'text') {
- if (document.activeElement !== this.input) {
- this.input.focus();
- }
- } else {
- if (this.canSearch) {
- this.showDropdown(true);
- } else {
- this.showDropdown();
- this.containerOuter.focus();
- }
- }
- } else if (this.passedElement.type === 'select-one' && target !== this.input && !this.dropdown.contains(target)) {
- this.hideDropdown(true);
- }
- } else {
- const hasHighlightedItems = activeItems.some((item) => item.highlighted === true);
-
- // De-select any highlighted items
- if (hasHighlightedItems) {
- this.unhighlightAll();
- }
-
- // Remove focus state
- this.containerOuter.classList.remove(this.config.classNames.focusState);
-
- // Close all other dropdowns
- if (hasActiveDropdown) {
- this.hideDropdown();
- }
- }
- }
-
- /**
- * Mouse over (hover) event
- * @param {Object} e Event
- * @return
- * @private
- */
- _onMouseOver(e) {
- // If the dropdown is either the target or one of its children is the target
- if (e.target === this.dropdown || this.dropdown.contains(e.target)) {
- if (e.target.hasAttribute('data-choice')) this._highlightChoice(e.target);
- }
- }
-
- /**
- * Paste event
- * @param {Object} e Event
- * @return
- * @private
- */
- _onPaste(e) {
- // Disable pasting into the input if option has been set
- if (e.target === this.input && !this.config.paste) {
- e.preventDefault();
- }
- }
-
- /**
- * Focus event
- * @param {Object} e Event
- * @return
- * @private
- */
- _onFocus(e) {
- const target = e.target;
- // If target is something that concerns us
- if (this.containerOuter.contains(target)) {
- const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
- const focusActions = {
- text: () => {
- if (target === this.input) {
- this.containerOuter.classList.add(this.config.classNames.focusState);
- }
- },
- 'select-one': () => {
- this.containerOuter.classList.add(this.config.classNames.focusState);
- if (target === this.input) {
- // Show dropdown if it isn't already showing
- if (!hasActiveDropdown) {
- this.showDropdown();
- }
- }
- },
- 'select-multiple': () => {
- if (target === this.input) {
- // If element is a select box, the focussed element is the container and the dropdown
- // isn't already open, focus and show dropdown
- this.containerOuter.classList.add(this.config.classNames.focusState);
-
- if (!hasActiveDropdown) {
- this.showDropdown(true);
- }
- }
- },
- };
-
- focusActions[this.passedElement.type]();
- }
- }
-
- /**
- * Blur event
- * @param {Object} e Event
- * @return
- * @private
- */
- _onBlur(e) {
- const target = e.target;
- // If target is something that concerns us
- if (this.containerOuter.contains(target)) {
- const activeItems = this.store.getItemsFilteredByActive();
- const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
- const hasHighlightedItems = activeItems.some((item) => item.highlighted === true);
- const blurActions = {
- text: () => {
- if (target === this.input) {
- // Remove the focus state
- this.containerOuter.classList.remove(this.config.classNames.focusState);
- // De-select any highlighted items
- if (hasHighlightedItems) {
- this.unhighlightAll();
- }
- // Hide dropdown if it is showing
- if (hasActiveDropdown) {
- this.hideDropdown();
- }
- }
- },
- 'select-one': () => {
- this.containerOuter.classList.remove(this.config.classNames.focusState);
- if (target === this.containerOuter) {
- // Hide dropdown if it is showing
- if (hasActiveDropdown && !this.canSearch) {
- this.hideDropdown();
- }
- }
-
- if (target === this.input) {
- // Hide dropdown if it is showing
- if (hasActiveDropdown) {
- this.hideDropdown();
- }
- }
- },
- 'select-multiple': () => {
- if (target === this.input) {
- // Remove the focus state
- this.containerOuter.classList.remove(this.config.classNames.focusState);
- if (hasActiveDropdown) {
- this.hideDropdown();
- }
- // De-select any highlighted items
- if (hasHighlightedItems) {
- this.unhighlightAll();
- }
- }
- },
- };
-
- blurActions[this.passedElement.type]();
- }
- }
-
- /**
- * Tests value against a regular expression
- * @param {string} value Value to test
- * @return {Boolean} Whether test passed/failed
- * @private
- */
- _regexFilter(value) {
- if (!value) return;
- const regex = this.config.regexFilter;
- const expression = new RegExp(regex.source, 'i');
- return expression.test(value);
- }
-
- /**
- * Scroll to an option element
- * @param {HTMLElement} option Option to scroll to
- * @param {Number} direction Whether option is above or below
- * @return
- * @private
- */
- _scrollToChoice(choice, direction) {
- if (!choice) return;
-
- const dropdownHeight = this.choiceList.offsetHeight;
- const choiceHeight = choice.offsetHeight;
-
- // Distance from bottom of element to top of parent
- const choicePos = choice.offsetTop + choiceHeight;
-
- // Scroll position of dropdown
- const containerScrollPos = this.choiceList.scrollTop + dropdownHeight;
-
- // Difference between the choice and scroll position
- const endPoint = direction > 0 ? ((this.choiceList.scrollTop + choicePos) - containerScrollPos) : choice.offsetTop;
-
- const animateScroll = () => {
- const strength = 4;
- let continueAnimation = false;
- let easing;
- let distance;
-
- if (direction > 0) {
- easing = (endPoint - this.choiceList.scrollTop) / strength;
- distance = easing > 1 ? easing : 1;
-
- this.choiceList.scrollTop = this.choiceList.scrollTop + distance;
- if (this.choiceList.scrollTop < endPoint) {
- continueAnimation = true;
- }
- } else {
- easing = (this.choiceList.scrollTop - endPoint) / strength;
- distance = easing > 1 ? easing : 1;
-
- this.choiceList.scrollTop = this.choiceList.scrollTop - distance;
- if (this.choiceList.scrollTop > endPoint) {
- continueAnimation = true;
- }
- }
-
- if (continueAnimation) {
- requestAnimationFrame((time) => {
- animateScroll(time, endPoint, direction);
- });
- }
- };
-
- requestAnimationFrame((time) => {
- animateScroll(time, endPoint, direction);
- });
- }
-
- /**
- * Highlight choice
- * @param {HTMLElement} el Element to highlight
- * @return
- * @private
- */
- _highlightChoice(el) {
- // Highlight first element in dropdown
- const choices = Array.from(this.dropdown.querySelectorAll('[data-choice-selectable]'));
+ return this;
+ }
+
+ /**
+ * Direct populate choices
+ * @param {Array} choices - Choices to insert
+ * @param {String} value - Name of 'value' property
+ * @param {String} label - Name of 'label' property
+ * @return {Object} Class instance
+ * @public
+ */
+ setChoices(choices, value, label) {
+ if (this.initialised === true) {
+ if (this.passedElement.type === 'select-one' || this.passedElement.type === 'select-multiple') {
+ if (!isType('Array', choices) || !value) return;
if (choices && choices.length) {
- const highlightedChoices = Array.from(this.dropdown.querySelectorAll(`.${this.config.classNames.highlightedState}`));
-
- // Remove any highlighted choices
- highlightedChoices.forEach((choice) => {
- choice.classList.remove(this.config.classNames.highlightedState);
- choice.setAttribute('aria-selected', 'false');
- });
-
- if (el) {
- // Highlight given option
- el.classList.add(this.config.classNames.highlightedState);
- this.highlightPosition = choices.indexOf(el);
+ this.containerOuter.classList.remove(this.config.classNames.loadingState);
+ choices.forEach((result, index) => {
+ if (result.choices) {
+ this._addGroup(result, index);
} else {
- // Highlight choice based on last known highlight location
- let choice;
-
- if (choices.length > this.highlightPosition) {
- // If we have an option to highlight
- choice = choices[this.highlightPosition];
- } else {
- // Otherwise highlight the option before
- choice = choices[choices.length - 1];
- }
-
- if (!choice) choice = choices[0];
- choice.classList.add(this.config.classNames.highlightedState);
- choice.setAttribute('aria-selected', 'true');
+ this._addChoice(result.selected ? result.selected : false, result.disabled ? result.disabled : false, result[value], result[label]);
}
+ });
}
+ }
}
+ return this;
+ }
- /**
- * Add item to store with correct value
- * @param {String} value Value to add to store
- * @param {String} label Label to add to store
- * @return {Object} Class instance
- * @public
- */
- _addItem(value, label, choiceId = -1) {
- let passedValue = isType('String', value) ? value.trim() : value;
- const items = this.store.getItems();
- const passedLabel = label || passedValue;
- const passedOptionId = parseInt(choiceId, 10) || -1;
+ /**
+ * Clear items,choices and groups
+ * @note Hard delete
+ * @return {Object} Class instance
+ * @public
+ */
+ clearStore() {
+ this.store.dispatch(clearAll());
+ return this;
+ }
- // If a prepended value has been passed, prepend it
- if (this.config.prependValue) {
- passedValue = this.config.prependValue + passedValue.toString();
- }
+ /**
+ * Set value of input to blank
+ * @return {Object} Class instance
+ * @public
+ */
+ clearInput() {
+ if (this.input.value) this.input.value = '';
+ if (this.passedElement.type !== 'select-one') {
+ this.input.style.width = getWidthOfInput(this.input);
+ }
+ if (this.passedElement.type !== 'text' && this.config.search) {
+ this.isSearching = false;
+ this.store.dispatch(activateChoices(true));
+ }
+ return this;
+ }
- // If an appended value has been passed, append it
- if (this.config.appendValue) {
- passedValue += this.config.appendValue.toString();
- }
+ /**
+ * Disable interaction with Choices
+ * @return {Object} Class instance
+ * @public
+ */
+ disable() {
+ this.passedElement.disabled = true;
+ if (this.initialised) {
+ if (!this.containerOuter.classList.contains(this.config.classNames.disabledState)) {
+ this._removeEventListeners();
+ this.passedElement.setAttribute('disabled', '');
+ this.input.setAttribute('disabled', '');
+ this.containerOuter.classList.add(this.config.classNames.disabledState);
+ this.containerOuter.setAttribute('aria-disabled', 'true');
+ }
+ }
+ return this;
+ }
- // Generate unique id
- const id = items ? items.length + 1 : 1;
-
- this.store.dispatch(addItem(passedValue, passedLabel, id, passedOptionId));
+ /**
+ * Enable interaction with Choices
+ * @return {Object} Class instance
+ */
+ enable() {
+ this.passedElement.disabled = false;
+ if (this.initialised) {
+ if (this.containerOuter.classList.contains(this.config.classNames.disabledState)) {
+ this._addEventListeners();
+ this.passedElement.removeAttribute('disabled');
+ this.input.removeAttribute('disabled');
+ this.containerOuter.classList.remove(this.config.classNames.disabledState);
+ this.containerOuter.removeAttribute('aria-disabled');
+ }
+ }
+ return this;
+ }
+ /**
+ * Populate options via ajax callback
+ * @param {Function} fn Passed
+ * @return {Object} Class instance
+ * @public
+ */
+ ajax(fn) {
+ if (this.initialised === true) {
+ if (this.passedElement.type === 'select-one' || this.passedElement.type === 'select-multiple') {
+ this.containerOuter.classList.add(this.config.classNames.loadingState);
+ this.containerOuter.setAttribute('aria-busy', 'true');
if (this.passedElement.type === 'select-one') {
- this.removeActiveItems(id);
- }
-
- // Run callback if it is a function
- if (this.config.callbackOnAddItem) {
- const callback = this.config.callbackOnAddItem;
- if (isType('Function', callback)) {
- callback(id, passedValue, this.passedElement);
- } else {
- console.error('callbackOnAddItem: Callback is not a function');
- }
- }
-
- return this;
- }
-
- /**
- * Remove item from store
- * @param {Object} item Item to remove
- * @param {Function} callback Callback to trigger
- * @return {Object} Class instance
- * @public
- */
- _removeItem(item, callback = this.config.callbackOnRemoveItem) {
- if (!item || !isType('Object', item)) {
- console.error('removeItem: No item object was passed to be removed');
- return;
- }
-
- const id = item.id;
- const value = item.value;
- const choiceId = item.choiceId;
-
- this.store.dispatch(removeItem(id, choiceId));
-
- // Run callback
- if (callback) {
- if (!isType('Function', callback)) {
- console.error('callbackOnRemoveItem: Callback is not a function');
- return;
- }
- callback(id, value, this.passedElement);
- }
-
- return this;
- }
-
- /**
- * Add choice to dropdown
- * @param {Boolean} isSelected Whether choice is selected
- * @param {Boolean} isDisabled Whether choice is disabled
- * @param {String} value Value of choice
- * @param {String} Label Label of choice
- * @param {Number} groupId ID of group choice is within. Negative number indicates no group
- * @return
- * @private
- */
- _addChoice(isSelected, isDisabled, value, label, groupId = -1) {
- if (!value) return;
-
- // Generate unique id
- const choices = this.store.getChoices();
- const choiceLabel = label || value;
- const choiceId = choices ? choices.length + 1 : 1;
-
- this.store.dispatch(addChoice(value, choiceLabel, choiceId, groupId, isDisabled));
-
- if (isSelected && !isDisabled) {
- this._addItem(value, choiceLabel, choiceId);
- }
- }
-
- /**
- * Add group to dropdown
- * @param {Object} group Group to add
- * @param {Number} id Group ID
- * @return
- * @private
- */
- _addGroup(group, id) {
- const groupChoices = isType('Object', group) ? group.choices : Array.from(group.getElementsByTagName('OPTION'));
- const groupId = id;
- const isDisabled = group.disabled ? group.disabled : false;
-
- if (groupChoices) {
- this.store.dispatch(addGroup(group.label, groupId, true, isDisabled));
-
- groupChoices.forEach((option) => {
- const isOptDisabled = (option.disabled || (option.parentNode && option.parentNode.disabled)) || false;
- const isOptSelected = option.selected ? option.selected : false;
- let label;
-
- if (isType('Object', option)) {
- label = option.label || option.value;
- } else {
- label = option.innerHTML;
- }
-
- this._addChoice(isOptSelected, isOptDisabled, option.value, label, groupId);
- });
+ const placeholderItem = this._getTemplate('placeholder', this.config.loadingText);
+ this.itemList.appendChild(placeholderItem);
} else {
- this.store.dispatch(addGroup(group.label, group.id, false, group.disabled));
+ this.input.placeholder = this.config.loadingText;
}
+
+ const callback = (results, value, label) => {
+ if (!isType('Array', results) || !value) return;
+ if (results && results.length) {
+ // Remove loading states/text
+ this.containerOuter.classList.remove(this.config.classNames.loadingState);
+ if (this.passedElement.type === 'select-multiple') {
+ const placeholder = this.config.placeholder ? this.config.placeholderValue || this.passedElement.getAttribute('placeholder') : false;
+ if (placeholder) {
+ this.input.placeholder = placeholder;
+ }
+ }
+
+ // Add each result as a choice
+ results.forEach((result, index) => {
+ this._addChoice(false, false, result[value], result[label]);
+ });
+ }
+ this.containerOuter.removeAttribute('aria-busy');
+ };
+ fn(callback);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Call change callback
+ * @param {String} value - last added/deleted/selected value
+ * @return
+ * @private
+ */
+ _triggerChange(value) {
+ if (!value) return;
+
+ // Run callback if it is a function
+ if (this.config.callbackOnChange) {
+ const callback = this.config.callbackOnChange;
+ if (isType('Function', callback)) {
+ callback(value, this.passedElement);
+ } else {
+ console.error('callbackOnChange: Callback is not a function');
+ }
+ }
+ }
+
+
+ /**
+ * Process enter/click of an item button
+ * @param {Array} activeItems The currently active items
+ * @param {Element} element Button being interacted with
+ * @return
+ * @private
+ */
+ _handleButtonAction(activeItems, element) {
+ if (!activeItems || !element) return;
+
+ // If we are clicking on a button
+ if (this.config.removeItems && this.config.removeItemButton) {
+ const itemId = element.parentNode.getAttribute('data-id');
+ const itemToRemove = activeItems.find((item) => item.id === parseInt(itemId, 10));
+
+ // Remove item associated with button
+ this._removeItem(itemToRemove);
+ this._triggerChange(itemToRemove.value);
+
+ if (this.passedElement.type === 'select-one') {
+ const placeholder = this.config.placeholder ? this.config.placeholderValue || this.passedElement.getAttribute('placeholder') : false;
+ if (placeholder) {
+ const placeholderItem = this._getTemplate('placeholder', placeholder);
+ this.itemList.appendChild(placeholderItem);
+ }
+ }
+ }
+ }
+
+ /**
+ * Process click of an item
+ * @param {Array} activeItems The currently active items
+ * @param {Element} element Item being interacted with
+ * @param {Boolean} hasShiftKey Whether the user has the shift key active
+ * @return
+ * @private
+ */
+ _handleItemAction(activeItems, element, hasShiftKey = false) {
+ if (!activeItems || !element) return;
+
+ // If we are clicking on an item
+ if (this.config.removeItems && this.passedElement.type !== 'select-one') {
+ const passedId = element.getAttribute('data-id');
+
+ // We only want to select one item with a click
+ // so we deselect any items that aren't the target
+ // unless shift is being pressed
+ activeItems.forEach((item) => {
+ if (item.id === parseInt(passedId, 10) && !item.highlighted) {
+ this.highlightItem(item);
+ } else if (!hasShiftKey) {
+ if (item.highlighted) {
+ this.unhighlightItem(item);
+ }
+ }
+ });
+
+ // Focus input as without focus, a user cannot do anything with a
+ // highlighted item
+ if (document.activeElement !== this.input) this.input.focus();
+ }
+ }
+
+ /**
+ * Process click of a choice
+ * @param {Array} activeItems The currently active items
+ * @param {Element} element Choice being interacted with
+ * @return {[type]} [description]
+ */
+ _handleChoiceAction(activeItems, element) {
+ if (!activeItems || !element) return;
+
+ // If we are clicking on an option
+ const id = element.getAttribute('data-id');
+ const choice = this.store.getChoiceById(id);
+ const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
+
+ if (choice && !choice.selected && !choice.disabled) {
+ const canAddItem = this._canAddItem(activeItems, choice.value);
+
+ if (canAddItem.response) {
+ this._addItem(choice.value, choice.label, choice.id);
+ this._triggerChange(choice.value);
+ }
}
- /**
- * Get template from name
- * @param {String} template Name of template to get
- * @param {...} args Data to pass to template
- * @return {HTMLElement} Template
- * @private
- */
- _getTemplate(template, ...args) {
- if (!template) return;
- const templates = this.config.templates;
- return templates[template](...args);
+ this.clearInput(this.passedElement);
+
+ // We wont to close the dropdown if we are dealing with a single select box
+ if (hasActiveDropdown && this.passedElement.type === 'select-one') {
+ this.hideDropdown();
+ this.containerOuter.focus();
+ }
+ }
+
+ /**
+ * Process back space event
+ * @param {Array} Active items
+ * @return
+ * @private
+ */
+ _handleBackspace(activeItems) {
+ if (this.config.removeItems && activeItems) {
+ const lastItem = activeItems[activeItems.length - 1];
+ const hasHighlightedItems = activeItems.some((item) => item.highlighted === true);
+
+ // If editing the last item is allowed and there are not other selected items,
+ // we can edit the item value. Otherwise if we can remove items, remove all selected items
+ if (this.config.editItems && !hasHighlightedItems && lastItem) {
+ this.input.value = lastItem.value;
+ this._removeItem(lastItem);
+ this._triggerChange(lastItem.value);
+ } else {
+ if (!hasHighlightedItems) {
+ this.highlightItem(lastItem);
+ }
+ this.removeHighlightedItems();
+ }
+ }
+ }
+
+ /**
+ * Validates whether an item can be added by a user
+ * @param {Array} activeItems The currently active items
+ * @param {String} value Value of item to add
+ * @return {Object} Response: Whether user can add item
+ * Notice: Notice show in dropdown
+ */
+ _canAddItem(activeItems, value) {
+ let canAddItem = true;
+ let notice = `Press Enter to add "${value}"`;
+
+ if (this.passedElement.type === 'select-multiple' || this.passedElement.type === 'text') {
+ if (this.config.maxItemCount > 0 && this.config.maxItemCount <= this.itemList.children.length) {
+ // If there is a max entry limit and we have reached that limit
+ // don't update
+ canAddItem = false;
+ notice = `Only ${this.config.maxItemCount} values can be added.`;
+ }
}
- /**
- * Create HTML element based on type and arguments
- * @return
- * @private
- */
- _createTemplates() {
- const classNames = this.config.classNames;
- const templates = {
- containerOuter: (direction) => {
- return strToEl(`
-
- `);
- },
- containerInner: () => {
- return strToEl(`
-
- `);
- },
- itemList: () => {
- return strToEl(`
-
- `);
- },
- placeholder: (value) => {
- return strToEl(`
-
- ${value}
-
- `);
- },
- item: (data) => {
- if (this.config.removeItemButton) {
- return strToEl(`
-
- ${data.label}
-
- `);
- }
- return strToEl(`
-
- ${data.label}
-
- `);
- },
- choiceList: () => {
- return strToEl(`
-
- `);
- },
- choiceGroup: (data) => {
- return strToEl(`
-
- `);
- },
- choice: (data) => {
- return strToEl(`
- 0 ? 'role="treeitem"' : 'role="option"'}>
- ${data.label}
-
- `);
- },
- input: () => {
- return strToEl(`
-
- `);
- },
- dropdown: () => {
- return strToEl(`
-
- `);
- },
- notice: (label) => {
- return strToEl(`
- ${label}
- `);
- },
- option: (data) => {
- return strToEl(`
-
- `);
- },
+ if (this.passedElement.type === 'text' && this.config.addItems) {
+ const isUnique = !activeItems.some((item) => item.value === value.trim());
+
+ // If a user has supplied a regular expression filter
+ if (this.config.regexFilter) {
+ // Determine whether we can update based on whether
+ // our regular expression passes
+ canAddItem = this._regexFilter(value);
+ }
+
+ // If no duplicates are allowed, and the value already exists
+ // in the array
+ if (this.config.duplicateItems === false && !isUnique) {
+ canAddItem = false;
+ notice = 'Only unique values can be added.';
+ }
+ }
+
+ return {
+ response: canAddItem,
+ notice,
+ };
+ }
+
+ /**
+ * Filter choices based on search value
+ * @param {String} value Value to filter by
+ * @return
+ * @private
+ */
+ _searchChoices(value) {
+ if (!value) return;
+ if (this.input === document.activeElement) {
+ const choices = this.store.getChoices();
+ const hasUnactiveChoices = choices.some((option) => option.active !== true);
+
+ // Check that we have a value to search and the input was an alphanumeric character
+ if (value && value.length > 1) {
+ const handleFilter = () => {
+ const newValue = isType('String', value) ? value.trim() : value;
+ const currentValue = isType('String', this.currentValue) ? this.currentValue.trim() : this.currentValue;
+
+ if (newValue.length >= 1 && newValue !== `${currentValue} `) {
+ const haystack = this.store.getChoicesFilteredBySelectable();
+ const needle = newValue;
+ const keys = isType('Array', this.config.sortFields) ? this.config.sortFields : [this.config.sortFields];
+ const fuse = new Fuse(haystack, {
+ keys,
+ shouldSort: true,
+ include: 'score',
+ });
+ const results = fuse.search(needle);
+
+ this.currentValue = newValue;
+ this.highlightPosition = 0;
+ this.isSearching = true;
+ this.store.dispatch(filterChoices(results));
+ }
};
- this.config.templates = templates;
+ handleFilter();
+ } else if (hasUnactiveChoices) {
+ // Otherwise reset choices to active
+ this.isSearching = false;
+ this.store.dispatch(activateChoices(true));
+ }
+ }
+ }
+
+ /**
+ * Trigger event listeners
+ * @return
+ * @private
+ */
+ _addEventListeners() {
+ document.addEventListener('keyup', this._onKeyUp);
+ document.addEventListener('keydown', this._onKeyDown);
+ document.addEventListener('click', this._onClick);
+ document.addEventListener('touchmove', this._onTouchMove);
+ document.addEventListener('touchend', this._onTouchEnd);
+ document.addEventListener('mousedown', this._onMouseDown);
+ document.addEventListener('mouseover', this._onMouseOver);
+
+ if (this.passedElement.type && this.passedElement.type === 'select-one') {
+ this.containerOuter.addEventListener('focus', this._onFocus);
+ this.containerOuter.addEventListener('blur', this._onBlur);
}
- /**
- * Create DOM structure around passed select element
- * @return
- * @private
- */
- _createInput() {
- const direction = this.passedElement.getAttribute('dir') || 'ltr';
- const containerOuter = this._getTemplate('containerOuter', direction);
- const containerInner = this._getTemplate('containerInner');
- const itemList = this._getTemplate('itemList');
- const choiceList = this._getTemplate('choiceList');
- const input = this._getTemplate('input');
- const dropdown = this._getTemplate('dropdown');
+ this.input.addEventListener('input', this._onInput);
+ this.input.addEventListener('paste', this._onPaste);
+ this.input.addEventListener('focus', this._onFocus);
+ this.input.addEventListener('blur', this._onBlur);
+ }
+
+ /**
+ * Destroy event listeners
+ * @return
+ * @private
+ */
+ _removeEventListeners() {
+ document.removeEventListener('keyup', this._onKeyUp);
+ document.removeEventListener('keydown', this._onKeyDown);
+ document.removeEventListener('click', this._onClick);
+ document.removeEventListener('touchmove', this._onTouchMove);
+ document.removeEventListener('touchend', this._onTouchEnd);
+ document.removeEventListener('mousedown', this._onMouseDown);
+ document.removeEventListener('mouseover', this._onMouseOver);
+
+ if (this.passedElement.type && this.passedElement.type === 'select-one') {
+ this.containerOuter.removeEventListener('focus', this._onFocus);
+ this.containerOuter.removeEventListener('blur', this._onBlur);
+ }
+
+ this.input.removeEventListener('input', this._onInput);
+ this.input.removeEventListener('paste', this._onPaste);
+ this.input.removeEventListener('focus', this._onFocus);
+ this.input.removeEventListener('blur', this._onBlur);
+ }
+
+ /**
+ * Key down event
+ * @param {Object} e Event
+ * @return
+ */
+ _onKeyDown(e) {
+ if (e.target !== this.input && !this.containerOuter.contains(e.target)) return;
+
+ const target = e.target;
+
+ const activeItems = this.store.getItemsFilteredByActive();
+ const hasFocusedInput = this.input === document.activeElement;
+ const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
+ const hasItems = this.itemList && this.itemList.children;
+ const keyString = String.fromCharCode(e.keyCode);
+
+ const backKey = 46;
+ const deleteKey = 8;
+ const enterKey = 13;
+ const aKey = 65;
+ const escapeKey = 27;
+ const upKey = 38;
+ const downKey = 40;
+ const ctrlDownKey = e.ctrlKey || e.metaKey;
+
+ // If a user is typing and the dropdown is not active
+ if (this.passedElement.type !== 'text' && /[a-zA-Z0-9-_ ]/.test(keyString) && !hasActiveDropdown) {
+ this.showDropdown(true);
+ }
+
+ this.canSearch = this.config.search;
+
+ const onAKey = () => {
+ // If CTRL + A or CMD + A have been pressed and there are items to select
+ if (ctrlDownKey && hasItems) {
+ this.canSearch = false;
+ if (this.config.removeItems && !this.input.value && this.input === document.activeElement) {
+ // Highlight items
+ this.highlightAll(this.itemList.children);
+ }
+ }
+ };
+
+ const onEnterKey = () => {
+ // If enter key is pressed and the input has a value
+ if (this.passedElement.type === 'text' && target.value) {
+ const value = this.input.value;
+ const canAddItem = this._canAddItem(activeItems, value);
+
+ // All is good, add
+ if (canAddItem.response) {
+ if (hasActiveDropdown) {
+ this.hideDropdown();
+ }
+ this._addItem(value);
+ this._triggerChange(value);
+ this.clearInput(this.passedElement);
+ }
+ }
+
+ if (target.hasAttribute('data-button')) {
+ this._handleButtonAction(activeItems, target);
+ e.preventDefault();
+ }
+
+ if (hasActiveDropdown) {
+ const highlighted = this.dropdown.querySelector(`.${this.config.classNames.highlightedState}`);
+
+ // If we have a highlighted choice
+ if (highlighted) {
+ this._handleChoiceAction(activeItems, highlighted);
+ }
+ } else if (this.passedElement.type === 'select-one') {
+ // Open single select dropdown if it's not active
+ if (!hasActiveDropdown) {
+ this.showDropdown(true);
+ e.preventDefault();
+ }
+ }
+ };
+
+ const onEscapeKey = () => {
+ if (hasActiveDropdown) {
+ this.toggleDropdown();
+ }
+ };
+
+ const onDirectionKey = () => {
+ // If up or down key is pressed, traverse through options
+ if (hasActiveDropdown || this.passedElement.type === 'select-one') {
+ // Show dropdown if focus
+ if (!hasActiveDropdown) {
+ this.showDropdown(true);
+ }
+
+ const currentEl = this.dropdown.querySelector(`.${this.config.classNames.highlightedState}`);
+ const directionInt = e.keyCode === downKey ? 1 : -1;
+ let nextEl;
+
+ this.canSearch = false;
+
+ if (currentEl) {
+ nextEl = getAdjacentEl(currentEl, '[data-choice-selectable]', directionInt);
+ } else {
+ nextEl = this.dropdown.querySelector('[data-choice-selectable]');
+ }
+
+ if (nextEl) {
+ // We prevent default to stop the cursor moving
+ // when pressing the arrow
+ if (!isScrolledIntoView(nextEl, this.choiceList, directionInt)) {
+ this._scrollToChoice(nextEl, directionInt);
+ }
+ this._highlightChoice(nextEl);
+ }
+
+ // Prevent default to maintain cursor position whilst
+ // traversing dropdown options
+ e.preventDefault();
+ }
+ };
+
+ const onDeleteKey = () => {
+ // If backspace or delete key is pressed and the input has no value
+ if (hasFocusedInput && !e.target.value && this.passedElement.type !== 'select-one') {
+ this._handleBackspace(activeItems);
+ e.preventDefault();
+ }
+ };
+
+ // Map keys to key actions
+ const keyDownActions = {
+ [aKey]: onAKey,
+ [enterKey]: onEnterKey,
+ [escapeKey]: onEscapeKey,
+ [upKey]: onDirectionKey,
+ [downKey]: onDirectionKey,
+ [deleteKey]: onDeleteKey,
+ [backKey]: onDeleteKey,
+ };
+
+ // If keycode has a function, run it
+ if (keyDownActions[e.keyCode]) {
+ keyDownActions[e.keyCode]();
+ }
+ }
+
+ /**
+ * Key up event
+ * @param {Object} e Event
+ * @return
+ * @private
+ */
+ _onKeyUp(e) {
+ if (e.target !== this.input) return;
+
+ // We are typing into a text input and have a value, we want to show a dropdown
+ // notice. Otherwise hide the dropdown
+ if (this.passedElement.type === 'text') {
+ const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
+ const value = this.input.value;
+
+ if (value) {
+ const activeItems = this.store.getItemsFilteredByActive();
+ const canAddItem = this._canAddItem(activeItems, value);
+
+ if (canAddItem.notice) {
+ const dropdownItem = this._getTemplate('notice', canAddItem.notice);
+ this.dropdown.innerHTML = dropdownItem.outerHTML;
+ }
+
+ if (canAddItem.response === true) {
+ if (!hasActiveDropdown) {
+ this.showDropdown();
+ }
+ } else if (!canAddItem.notice && hasActiveDropdown) {
+ this.hideDropdown();
+ }
+ } else if (hasActiveDropdown) {
+ this.hideDropdown();
+ }
+ } else {
+ const backKey = 46;
+ const deleteKey = 8;
+
+ // If user has removed value...
+ if ((e.keyCode === backKey || e.keyCode === deleteKey) && !e.target.value) {
+ // ...and it is a multiple select input, activate choices (if searching)
+ if (this.passedElement.type !== 'text' && this.isSearching) {
+ this.isSearching = false;
+ this.store.dispatch(activateChoices(true));
+ }
+ } else if (this.canSearch) {
+ this._searchChoices(this.input.value);
+ }
+ }
+ }
+
+ /**
+ * Input event
+ * @param {Object} e Event
+ * @return
+ * @private
+ */
+ _onInput() {
+ if (this.passedElement.type !== 'select-one') {
+ if (this.config.placeholder && (this.config.placeholderValue || this.passedElement.getAttribute('placeholder'))) {
+ // If there is a placeholder, we only want to set the width of the input when it is a greater
+ // length than 75% of the placeholder. This stops the input jumping around.
const placeholder = this.config.placeholder ? this.config.placeholderValue || this.passedElement.getAttribute('placeholder') : false;
-
- this.containerOuter = containerOuter;
- this.containerInner = containerInner;
- this.input = input;
- this.choiceList = choiceList;
- this.itemList = itemList;
- this.dropdown = dropdown;
-
- // Hide passed input
- this.passedElement.classList.add(this.config.classNames.input, this.config.classNames.hiddenState);
- this.passedElement.tabIndex = '-1';
- this.passedElement.setAttribute('style', 'display:none;');
- this.passedElement.setAttribute('aria-hidden', 'true');
- this.passedElement.setAttribute('data-choice', 'active');
-
- // Wrap input in container preserving DOM ordering
- wrap(this.passedElement, containerInner);
-
- // Wrapper inner container with outer container
- wrap(containerInner, containerOuter);
-
- // If placeholder has been enabled and we have a value
- if (placeholder) {
- input.placeholder = placeholder;
- if (this.passedElement.type !== 'select-one') {
- input.style.width = getWidthOfInput(input);
- }
+ if (this.input.value && this.input.value.length >= (placeholder.length / 1.25)) {
+ this.input.style.width = getWidthOfInput(this.input);
}
+ } else {
+ // If there is no placeholder, resize input to contents
+ this.input.style.width = getWidthOfInput(this.input);
+ }
+ }
+ }
- if (!this.config.addItems) this.disable();
+ /**
+ * Touch move event
+ * @param {Object} e Event
+ * @return
+ * @private
+ */
+ _onTouchMove() {
+ if (this.wasTap === true) {
+ this.wasTap = false;
+ }
+ }
- containerOuter.appendChild(containerInner);
- containerOuter.appendChild(dropdown);
- containerInner.appendChild(itemList);
+ /**
+ * Touch end event
+ * @param {Object} e Event
+ * @return
+ * @private
+ */
+ _onTouchEnd(e) {
+ const target = e.target || e.touches[0].target;
+ const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
- if (this.passedElement.type !== 'text') {
- dropdown.appendChild(choiceList);
- }
-
- if (this.passedElement.type === 'select-multiple' || this.passedElement.type === 'text') {
- containerInner.appendChild(input);
- } else if (this.canSearch) {
- dropdown.insertBefore(input, dropdown.firstChild);
- }
-
- if (this.passedElement.type === 'select-multiple' || this.passedElement.type === 'select-one') {
- const passedGroups = Array.from(this.passedElement.getElementsByTagName('OPTGROUP'));
-
- this.highlightPosition = 0;
- this.isSearching = false;
-
- if (passedGroups && passedGroups.length) {
- passedGroups.forEach((group, index) => {
- this._addGroup(group, index);
- });
- } else {
- const passedOptions = Array.from(this.passedElement.options);
- const filter = this.config.sortFilter;
- 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,
- });
- });
-
- // If sorting is enabled or the user is searching, filter choices
- if (this.config.shouldSort) {
- allChoices.sort(filter);
- }
-
- // Determine whether there is a selected choice
- const hasSelectedChoice = allChoices.some((choice) => {
- return choice.selected === true;
- });
-
- // Add each choice
- allChoices.forEach((choice, index) => {
- const isDisabled = choice.disabled ? choice.disabled : false;
- const isSelected = choice.selected ? choice.selected : false;
- // Pre-select first choice if it's a single select
- if (this.passedElement.type === 'select-one') {
- if (hasSelectedChoice || (!hasSelectedChoice && index > 0)) {
- // If there is a selected choice already or the choice is not
- // the first in the array, add each choice normally
- this._addChoice(isSelected, isDisabled, choice.value, choice.label);
- } else {
- // Otherwise pre-select the first choice in the array
- this._addChoice(true, false, choice.value, choice.label);
- }
- } else {
- this._addChoice(isSelected, isDisabled, choice.value, choice.label);
- }
- });
- }
- } else if (this.passedElement.type === 'text') {
- // Add any preset values seperated by delimiter
- this.presetItems.forEach((item) => {
- if (isType('Object', item)) {
- if (!item.value) return;
- this._addItem(item.value, item.label, item.id);
- } else if (isType('String', item)) {
- this._addItem(item);
- }
- });
+ // If a user tapped within our container...
+ if (this.wasTap === true && this.containerOuter.contains(target)) {
+ // ...and we aren't dealing with a single select box, show dropdown/focus input
+ if ((target === this.containerOuter || target === this.containerInner) && this.passedElement.type !== 'select-one') {
+ if (this.passedElement.type === 'text') {
+ // If text element, we only want to focus the input (if it isn't already)
+ if (document.activeElement !== this.input) {
+ this.input.focus();
+ }
+ } else {
+ if (!hasActiveDropdown) {
+ // If a select box, we want to show the dropdown
+ this.showDropdown(true);
+ }
}
+ }
+ // Prevents focus event firing
+ e.stopPropagation();
}
- /**
- * Render group choices into a DOM fragment and append to choice list
- * @param {Array} groups Groups to add to list
- * @param {Array} choices Choices to add to groups
- * @param {DocumentFragment} fragment Fragment to add groups and options to (optional)
- * @return {DocumentFragment} Populated options fragment
- * @private
- */
- renderGroups(groups, choices, fragment) {
- const groupFragment = fragment || document.createDocumentFragment();
- const filter = this.config.sortFilter;
+ this.wasTap = true;
+ }
- // If sorting is enabled, filter groups
- if (this.config.shouldSort) {
- groups.sort(filter);
+ /**
+ * Mouse down event
+ * @param {Object} e Event
+ * @return
+ * @private
+ */
+ _onMouseDown(e) {
+ const target = e.target;
+
+ if (this.containerOuter.contains(target) && target !== this.input) {
+ const activeItems = this.store.getItemsFilteredByActive();
+ const hasShiftKey = e.shiftKey;
+
+ if (target.hasAttribute('data-item')) {
+ this._handleItemAction(activeItems, target, hasShiftKey);
+ } else if (target.hasAttribute('data-choice')) {
+ this._handleChoiceAction(activeItems, target);
+ }
+
+ e.preventDefault();
+ }
+ }
+
+ /**
+ * Click event
+ * @param {Object} e Event
+ * @return
+ * @private
+ */
+ _onClick(e) {
+ const target = e.target;
+ const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
+ const activeItems = this.store.getItemsFilteredByActive();
+
+ // If target is something that concerns us
+ if (this.containerOuter.contains(target)) {
+ // Handle button delete
+ if (target.hasAttribute('data-button')) {
+ this._handleButtonAction(activeItems, target);
+ }
+
+ if (!hasActiveDropdown) {
+ if (this.passedElement.type === 'text') {
+ if (document.activeElement !== this.input) {
+ this.input.focus();
+ }
+ } else {
+ if (this.canSearch) {
+ this.showDropdown(true);
+ } else {
+ this.showDropdown();
+ this.containerOuter.focus();
+ }
}
+ } else if (this.passedElement.type === 'select-one' && target !== this.input && !this.dropdown.contains(target)) {
+ this.hideDropdown(true);
+ }
+ } else {
+ const hasHighlightedItems = activeItems.some((item) => item.highlighted === true);
- groups.forEach((group) => {
- // Grab options that are children of this group
- const groupChoices = choices.filter((choice) => {
- if (this.passedElement.type === 'select-one') {
- return choice.groupId === group.id;
- }
+ // De-select any highlighted items
+ if (hasHighlightedItems) {
+ this.unhighlightAll();
+ }
- return choice.groupId === group.id && !choice.selected;
- });
+ // Remove focus state
+ this.containerOuter.classList.remove(this.config.classNames.focusState);
- if (groupChoices.length >= 1) {
- const dropdownGroup = this._getTemplate('choiceGroup', group);
- groupFragment.appendChild(dropdownGroup);
- this.renderChoices(groupChoices, groupFragment);
+ // Close all other dropdowns
+ if (hasActiveDropdown) {
+ this.hideDropdown();
+ }
+ }
+ }
+
+ /**
+ * Mouse over (hover) event
+ * @param {Object} e Event
+ * @return
+ * @private
+ */
+ _onMouseOver(e) {
+ // If the dropdown is either the target or one of its children is the target
+ if (e.target === this.dropdown || this.dropdown.contains(e.target)) {
+ if (e.target.hasAttribute('data-choice')) this._highlightChoice(e.target);
+ }
+ }
+
+ /**
+ * Paste event
+ * @param {Object} e Event
+ * @return
+ * @private
+ */
+ _onPaste(e) {
+ // Disable pasting into the input if option has been set
+ if (e.target === this.input && !this.config.paste) {
+ e.preventDefault();
+ }
+ }
+
+ /**
+ * Focus event
+ * @param {Object} e Event
+ * @return
+ * @private
+ */
+ _onFocus(e) {
+ const target = e.target;
+ // If target is something that concerns us
+ if (this.containerOuter.contains(target)) {
+ const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
+ const focusActions = {
+ text: () => {
+ if (target === this.input) {
+ this.containerOuter.classList.add(this.config.classNames.focusState);
+ }
+ },
+ 'select-one': () => {
+ this.containerOuter.classList.add(this.config.classNames.focusState);
+ if (target === this.input) {
+ // Show dropdown if it isn't already showing
+ if (!hasActiveDropdown) {
+ this.showDropdown();
}
+ }
+ },
+ 'select-multiple': () => {
+ if (target === this.input) {
+ // If element is a select box, the focussed element is the container and the dropdown
+ // isn't already open, focus and show dropdown
+ this.containerOuter.classList.add(this.config.classNames.focusState);
+
+ if (!hasActiveDropdown) {
+ this.showDropdown(true);
+ }
+ }
+ },
+ };
+
+ focusActions[this.passedElement.type]();
+ }
+ }
+
+ /**
+ * Blur event
+ * @param {Object} e Event
+ * @return
+ * @private
+ */
+ _onBlur(e) {
+ const target = e.target;
+ // If target is something that concerns us
+ if (this.containerOuter.contains(target)) {
+ const activeItems = this.store.getItemsFilteredByActive();
+ const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
+ const hasHighlightedItems = activeItems.some((item) => item.highlighted === true);
+ const blurActions = {
+ text: () => {
+ if (target === this.input) {
+ // Remove the focus state
+ this.containerOuter.classList.remove(this.config.classNames.focusState);
+ // De-select any highlighted items
+ if (hasHighlightedItems) {
+ this.unhighlightAll();
+ }
+ // Hide dropdown if it is showing
+ if (hasActiveDropdown) {
+ this.hideDropdown();
+ }
+ }
+ },
+ 'select-one': () => {
+ this.containerOuter.classList.remove(this.config.classNames.focusState);
+ if (target === this.containerOuter) {
+ // Hide dropdown if it is showing
+ if (hasActiveDropdown && !this.canSearch) {
+ this.hideDropdown();
+ }
+ }
+
+ if (target === this.input) {
+ // Hide dropdown if it is showing
+ if (hasActiveDropdown) {
+ this.hideDropdown();
+ }
+ }
+ },
+ 'select-multiple': () => {
+ if (target === this.input) {
+ // Remove the focus state
+ this.containerOuter.classList.remove(this.config.classNames.focusState);
+ if (hasActiveDropdown) {
+ this.hideDropdown();
+ }
+ // De-select any highlighted items
+ if (hasHighlightedItems) {
+ this.unhighlightAll();
+ }
+ }
+ },
+ };
+
+ blurActions[this.passedElement.type]();
+ }
+ }
+
+ /**
+ * Tests value against a regular expression
+ * @param {string} value Value to test
+ * @return {Boolean} Whether test passed/failed
+ * @private
+ */
+ _regexFilter(value) {
+ if (!value) return;
+ const regex = this.config.regexFilter;
+ const expression = new RegExp(regex.source, 'i');
+ return expression.test(value);
+ }
+
+ /**
+ * Scroll to an option element
+ * @param {HTMLElement} option Option to scroll to
+ * @param {Number} direction Whether option is above or below
+ * @return
+ * @private
+ */
+ _scrollToChoice(choice, direction) {
+ if (!choice) return;
+
+ const dropdownHeight = this.choiceList.offsetHeight;
+ const choiceHeight = choice.offsetHeight;
+
+ // Distance from bottom of element to top of parent
+ const choicePos = choice.offsetTop + choiceHeight;
+
+ // Scroll position of dropdown
+ const containerScrollPos = this.choiceList.scrollTop + dropdownHeight;
+
+ // Difference between the choice and scroll position
+ const endPoint = direction > 0 ? ((this.choiceList.scrollTop + choicePos) - containerScrollPos) : choice.offsetTop;
+
+ const animateScroll = () => {
+ const strength = 4;
+ let continueAnimation = false;
+ let easing;
+ let distance;
+
+ if (direction > 0) {
+ easing = (endPoint - this.choiceList.scrollTop) / strength;
+ distance = easing > 1 ? easing : 1;
+
+ this.choiceList.scrollTop = this.choiceList.scrollTop + distance;
+ if (this.choiceList.scrollTop < endPoint) {
+ continueAnimation = true;
+ }
+ } else {
+ easing = (this.choiceList.scrollTop - endPoint) / strength;
+ distance = easing > 1 ? easing : 1;
+
+ this.choiceList.scrollTop = this.choiceList.scrollTop - distance;
+ if (this.choiceList.scrollTop > endPoint) {
+ continueAnimation = true;
+ }
+ }
+
+ if (continueAnimation) {
+ requestAnimationFrame((time) => {
+ animateScroll(time, endPoint, direction);
});
+ }
+ };
- return groupFragment;
+ requestAnimationFrame((time) => {
+ animateScroll(time, endPoint, direction);
+ });
+ }
+
+ /**
+ * Highlight choice
+ * @param {HTMLElement} el Element to highlight
+ * @return
+ * @private
+ */
+ _highlightChoice(el) {
+ // Highlight first element in dropdown
+ const choices = Array.from(this.dropdown.querySelectorAll('[data-choice-selectable]'));
+
+ if (choices && choices.length) {
+ const highlightedChoices = Array.from(this.dropdown.querySelectorAll(`.${this.config.classNames.highlightedState}`));
+
+ // Remove any highlighted choices
+ highlightedChoices.forEach((choice) => {
+ choice.classList.remove(this.config.classNames.highlightedState);
+ choice.setAttribute('aria-selected', 'false');
+ });
+
+ if (el) {
+ // Highlight given option
+ el.classList.add(this.config.classNames.highlightedState);
+ this.highlightPosition = choices.indexOf(el);
+ } else {
+ // Highlight choice based on last known highlight location
+ let choice;
+
+ if (choices.length > this.highlightPosition) {
+ // If we have an option to highlight
+ choice = choices[this.highlightPosition];
+ } else {
+ // Otherwise highlight the option before
+ choice = choices[choices.length - 1];
+ }
+
+ if (!choice) choice = choices[0];
+ choice.classList.add(this.config.classNames.highlightedState);
+ choice.setAttribute('aria-selected', 'true');
+ }
+ }
+ }
+
+ /**
+ * Add item to store with correct value
+ * @param {String} value Value to add to store
+ * @param {String} label Label to add to store
+ * @return {Object} Class instance
+ * @public
+ */
+ _addItem(value, label, choiceId = -1) {
+ let passedValue = isType('String', value) ? value.trim() : value;
+ const items = this.store.getItems();
+ const passedLabel = label || passedValue;
+ const passedOptionId = parseInt(choiceId, 10) || -1;
+
+ // If a prepended value has been passed, prepend it
+ if (this.config.prependValue) {
+ passedValue = this.config.prependValue + passedValue.toString();
}
- /**
- * Render choices into a DOM fragment and append to choice list
- * @param {Array} choices Choices to add to list
- * @param {DocumentFragment} fragment Fragment to add choices to (optional)
- * @return {DocumentFragment} Populated choices fragment
- * @private
- */
- renderChoices(choices, fragment) {
- // Create a fragment to store our list items (so we don't have to update the DOM for each item)
- const choicesFragment = fragment || document.createDocumentFragment();
- const filter = this.isSearching ? sortByScore : this.config.sortFilter;
+ // If an appended value has been passed, append it
+ if (this.config.appendValue) {
+ passedValue += this.config.appendValue.toString();
+ }
+
+ // Generate unique id
+ const id = items ? items.length + 1 : 1;
+
+ this.store.dispatch(addItem(passedValue, passedLabel, id, passedOptionId));
+
+ if (this.passedElement.type === 'select-one') {
+ this.removeActiveItems(id);
+ }
+
+ // Run callback if it is a function
+ if (this.config.callbackOnAddItem) {
+ const callback = this.config.callbackOnAddItem;
+ if (isType('Function', callback)) {
+ callback(id, passedValue, this.passedElement);
+ } else {
+ console.error('callbackOnAddItem: Callback is not a function');
+ }
+ }
+
+ return this;
+ }
+
+ /**
+ * Remove item from store
+ * @param {Object} item Item to remove
+ * @param {Function} callback Callback to trigger
+ * @return {Object} Class instance
+ * @public
+ */
+ _removeItem(item, callback = this.config.callbackOnRemoveItem) {
+ if (!item || !isType('Object', item)) {
+ console.error('removeItem: No item object was passed to be removed');
+ return;
+ }
+
+ const id = item.id;
+ const value = item.value;
+ const choiceId = item.choiceId;
+
+ this.store.dispatch(removeItem(id, choiceId));
+
+ // Run callback
+ if (callback) {
+ if (!isType('Function', callback)) {
+ console.error('callbackOnRemoveItem: Callback is not a function');
+ return;
+ }
+ callback(id, value, this.passedElement);
+ }
+
+ return this;
+ }
+
+ /**
+ * Add choice to dropdown
+ * @param {Boolean} isSelected Whether choice is selected
+ * @param {Boolean} isDisabled Whether choice is disabled
+ * @param {String} value Value of choice
+ * @param {String} Label Label of choice
+ * @param {Number} groupId ID of group choice is within. Negative number indicates no group
+ * @return
+ * @private
+ */
+ _addChoice(isSelected, isDisabled, value, label, groupId = -1) {
+ if (!value) return;
+
+ // Generate unique id
+ const choices = this.store.getChoices();
+ const choiceLabel = label || value;
+ const choiceId = choices ? choices.length + 1 : 1;
+
+ this.store.dispatch(addChoice(value, choiceLabel, choiceId, groupId, isDisabled));
+
+ if (isSelected && !isDisabled) {
+ this._addItem(value, choiceLabel, choiceId);
+ }
+ }
+
+ /**
+ * Add group to dropdown
+ * @param {Object} group Group to add
+ * @param {Number} id Group ID
+ * @return
+ * @private
+ */
+ _addGroup(group, id) {
+ const groupChoices = isType('Object', group) ? group.choices : Array.from(group.getElementsByTagName('OPTION'));
+ const groupId = id;
+ const isDisabled = group.disabled ? group.disabled : false;
+
+ if (groupChoices) {
+ this.store.dispatch(addGroup(group.label, groupId, true, isDisabled));
+
+ groupChoices.forEach((option) => {
+ const isOptDisabled = (option.disabled || (option.parentNode && option.parentNode.disabled)) || false;
+ const isOptSelected = option.selected ? option.selected : false;
+ let label;
+
+ if (isType('Object', option)) {
+ label = option.label || option.value;
+ } else {
+ label = option.innerHTML;
+ }
+
+ this._addChoice(isOptSelected, isOptDisabled, option.value, label, groupId);
+ });
+ } else {
+ this.store.dispatch(addGroup(group.label, group.id, false, group.disabled));
+ }
+ }
+
+ /**
+ * Get template from name
+ * @param {String} template Name of template to get
+ * @param {...} args Data to pass to template
+ * @return {HTMLElement} Template
+ * @private
+ */
+ _getTemplate(template, ...args) {
+ if (!template) return;
+ const templates = this.config.templates;
+ return templates[template](...args);
+ }
+
+ /**
+ * Create HTML element based on type and arguments
+ * @return
+ * @private
+ */
+ _createTemplates() {
+ const classNames = this.config.classNames;
+ const templates = {
+ containerOuter: (direction) => {
+ return strToEl(`
+
+ `);
+ },
+ containerInner: () => {
+ return strToEl(`
+
+ `);
+ },
+ itemList: () => {
+ return strToEl(`
+
+ `);
+ },
+ placeholder: (value) => {
+ return strToEl(`
+
+ ${value}
+
+ `);
+ },
+ item: (data) => {
+ if (this.config.removeItemButton) {
+ return strToEl(`
+
+ ${data.label}
+
+ `);
+ }
+ return strToEl(`
+
+ ${data.label}
+
+ `);
+ },
+ choiceList: () => {
+ return strToEl(`
+
+ `);
+ },
+ choiceGroup: (data) => {
+ return strToEl(`
+
+ `);
+ },
+ choice: (data) => {
+ return strToEl(`
+ 0 ? 'role="treeitem"' : 'role="option"'}>
+ ${data.label}
+
+ `);
+ },
+ input: () => {
+ return strToEl(`
+
+ `);
+ },
+ dropdown: () => {
+ return strToEl(`
+
+ `);
+ },
+ notice: (label) => {
+ return strToEl(`
+ ${label}
+ `);
+ },
+ option: (data) => {
+ return strToEl(`
+
+ `);
+ },
+ };
+
+ this.config.templates = templates;
+ }
+
+ /**
+ * Create DOM structure around passed select element
+ * @return
+ * @private
+ */
+ _createInput() {
+ const direction = this.passedElement.getAttribute('dir') || 'ltr';
+ const containerOuter = this._getTemplate('containerOuter', direction);
+ const containerInner = this._getTemplate('containerInner');
+ const itemList = this._getTemplate('itemList');
+ const choiceList = this._getTemplate('choiceList');
+ const input = this._getTemplate('input');
+ const dropdown = this._getTemplate('dropdown');
+ const placeholder = this.config.placeholder ? this.config.placeholderValue || this.passedElement.getAttribute('placeholder') : false;
+
+ this.containerOuter = containerOuter;
+ this.containerInner = containerInner;
+ this.input = input;
+ this.choiceList = choiceList;
+ this.itemList = itemList;
+ this.dropdown = dropdown;
+
+ // Hide passed input
+ this.passedElement.classList.add(this.config.classNames.input, this.config.classNames.hiddenState);
+ this.passedElement.tabIndex = '-1';
+ this.passedElement.setAttribute('style', 'display:none;');
+ this.passedElement.setAttribute('aria-hidden', 'true');
+ this.passedElement.setAttribute('data-choice', 'active');
+
+ // Wrap input in container preserving DOM ordering
+ wrap(this.passedElement, containerInner);
+
+ // Wrapper inner container with outer container
+ wrap(containerInner, containerOuter);
+
+ // If placeholder has been enabled and we have a value
+ if (placeholder) {
+ input.placeholder = placeholder;
+ if (this.passedElement.type !== 'select-one') {
+ input.style.width = getWidthOfInput(input);
+ }
+ }
+
+ if (!this.config.addItems) this.disable();
+
+ containerOuter.appendChild(containerInner);
+ containerOuter.appendChild(dropdown);
+ containerInner.appendChild(itemList);
+
+ if (this.passedElement.type !== 'text') {
+ dropdown.appendChild(choiceList);
+ }
+
+ if (this.passedElement.type === 'select-multiple' || this.passedElement.type === 'text') {
+ containerInner.appendChild(input);
+ } else if (this.canSearch) {
+ dropdown.insertBefore(input, dropdown.firstChild);
+ }
+
+ if (this.passedElement.type === 'select-multiple' || this.passedElement.type === 'select-one') {
+ const passedGroups = Array.from(this.passedElement.getElementsByTagName('OPTGROUP'));
+
+ this.highlightPosition = 0;
+ this.isSearching = false;
+
+ if (passedGroups && passedGroups.length) {
+ passedGroups.forEach((group, index) => {
+ this._addGroup(group, index);
+ });
+ } else {
+ const passedOptions = Array.from(this.passedElement.options);
+ const filter = this.config.sortFilter;
+ 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,
+ });
+ });
// If sorting is enabled or the user is searching, filter choices
- if (this.config.shouldSort || this.isSearching) {
- choices.sort(filter);
+ if (this.config.shouldSort) {
+ allChoices.sort(filter);
}
- choices.forEach((choice) => {
- const dropdownItem = this._getTemplate('choice', choice);
- const shouldRender = this.passedElement.type === 'select-one' || !choice.selected;
- if (shouldRender) {
- choicesFragment.appendChild(dropdownItem);
- }
+ // Determine whether there is a selected choice
+ const hasSelectedChoice = allChoices.some((choice) => {
+ return choice.selected === true;
});
- return choicesFragment;
- }
-
- /**
- * Render items into a DOM fragment and append to items list
- * @param {Array} items Items to add to list
- * @param {DocumentFragment} fragment Fragrment to add items to (optional)
- * @return
- * @private
- */
- renderItems(items, fragment) {
- // Create fragment to add elements to
- const itemListFragment = fragment || document.createDocumentFragment();
- // Simplify store data to just values
- const itemsFiltered = this.store.getItemsReducedToValues(items);
-
- if (this.passedElement.type === 'text') {
- // Assign hidden input array of values
- this.passedElement.setAttribute('value', itemsFiltered.join(this.config.delimiter));
- } else {
- const selectedOptionsFragment = document.createDocumentFragment();
-
- // Add each list item to list
- items.forEach((item) => {
- // Create a standard select option
- const option = this._getTemplate('option', item);
- // Append it to fragment
- selectedOptionsFragment.appendChild(option);
- });
-
- // Update selected choices
- this.passedElement.innerHTML = '';
- this.passedElement.appendChild(selectedOptionsFragment);
- }
-
- // Add each list item to list
- items.forEach((item) => {
- // Create new list element
- const listItem = this._getTemplate('item', item);
- // Append it to list
- itemListFragment.appendChild(listItem);
+ // Add each choice
+ allChoices.forEach((choice, index) => {
+ const isDisabled = choice.disabled ? choice.disabled : false;
+ const isSelected = choice.selected ? choice.selected : false;
+ // Pre-select first choice if it's a single select
+ if (this.passedElement.type === 'select-one') {
+ if (hasSelectedChoice || (!hasSelectedChoice && index > 0)) {
+ // If there is a selected choice already or the choice is not
+ // the first in the array, add each choice normally
+ this._addChoice(isSelected, isDisabled, choice.value, choice.label);
+ } else {
+ // Otherwise pre-select the first choice in the array
+ this._addChoice(true, false, choice.value, choice.label);
+ }
+ } else {
+ this._addChoice(isSelected, isDisabled, choice.value, choice.label);
+ }
});
-
- return itemListFragment;
- }
-
- /**
- * Render DOM with values
- * @return
- * @private
- */
- render() {
- this.currentState = this.store.getState();
-
- // Only render if our state has actually changed
- if (this.currentState !== this.prevState) {
- // Choices
- if (this.currentState.choices !== this.prevState.choices || this.currentState.groups !== this.prevState.groups) {
- if (this.passedElement.type === 'select-multiple' || this.passedElement.type === 'select-one') {
- // Get active groups/choices
- const activeGroups = this.store.getGroupsFilteredByActive();
- const activeChoices = this.store.getChoicesFilteredByActive();
-
- let choiceListFragment = document.createDocumentFragment();
-
- // Clear choices
- this.choiceList.innerHTML = '';
- // Scroll back to top of choices list
- this.choiceList.scrollTop = 0;
-
- // If we have grouped options
- if (activeGroups.length >= 1 && this.isSearching !== true) {
- choiceListFragment = this.renderGroups(activeGroups, activeChoices, choiceListFragment);
- } else if (activeChoices.length >= 1) {
- choiceListFragment = this.renderChoices(activeChoices, choiceListFragment);
- }
-
- if (choiceListFragment.childNodes && choiceListFragment.childNodes.length > 0) {
- // If we actually have anything to add to our dropdown
- // append it and highlight the first choice
- this.choiceList.appendChild(choiceListFragment);
- this._highlightChoice();
- } else {
- // Otherwise show a notice
- const dropdownItem = this.isSearching ? this._getTemplate('notice', this.config.noResultsText) : this._getTemplate('notice', this.config.noChoicesText);
- this.choiceList.appendChild(dropdownItem);
- }
- }
- }
-
- // Items
- if (this.currentState.items !== this.prevState.items) {
- const activeItems = this.store.getItemsFilteredByActive();
- if (activeItems) {
- // Create a fragment to store our list items
- // (so we don't have to update the DOM for each item)
- const itemListFragment = this.renderItems(activeItems);
-
- // Clear list
- this.itemList.innerHTML = '';
-
- // If we have items to add
- if (itemListFragment.childNodes) {
- // Update list
- this.itemList.appendChild(itemListFragment);
- }
- }
- }
-
- this.prevState = this.currentState;
+ }
+ } else if (this.passedElement.type === 'text') {
+ // Add any preset values seperated by delimiter
+ this.presetItems.forEach((item) => {
+ if (isType('Object', item)) {
+ if (!item.value) return;
+ this._addItem(item.value, item.label, item.id);
+ } else if (isType('String', item)) {
+ this._addItem(item);
}
+ });
}
+ }
+
+ /**
+ * Render group choices into a DOM fragment and append to choice list
+ * @param {Array} groups Groups to add to list
+ * @param {Array} choices Choices to add to groups
+ * @param {DocumentFragment} fragment Fragment to add groups and options to (optional)
+ * @return {DocumentFragment} Populated options fragment
+ * @private
+ */
+ renderGroups(groups, choices, fragment) {
+ const groupFragment = fragment || document.createDocumentFragment();
+ const filter = this.config.sortFilter;
+
+ // If sorting is enabled, filter groups
+ if (this.config.shouldSort) {
+ groups.sort(filter);
+ }
+
+ groups.forEach((group) => {
+ // Grab options that are children of this group
+ const groupChoices = choices.filter((choice) => {
+ if (this.passedElement.type === 'select-one') {
+ return choice.groupId === group.id;
+ }
+
+ return choice.groupId === group.id && !choice.selected;
+ });
+
+ if (groupChoices.length >= 1) {
+ const dropdownGroup = this._getTemplate('choiceGroup', group);
+ groupFragment.appendChild(dropdownGroup);
+ this.renderChoices(groupChoices, groupFragment);
+ }
+ });
+
+ return groupFragment;
+ }
+
+ /**
+ * Render choices into a DOM fragment and append to choice list
+ * @param {Array} choices Choices to add to list
+ * @param {DocumentFragment} fragment Fragment to add choices to (optional)
+ * @return {DocumentFragment} Populated choices fragment
+ * @private
+ */
+ renderChoices(choices, fragment) {
+ // Create a fragment to store our list items (so we don't have to update the DOM for each item)
+ const choicesFragment = fragment || document.createDocumentFragment();
+ const filter = this.isSearching ? sortByScore : this.config.sortFilter;
+
+ // If sorting is enabled or the user is searching, filter choices
+ if (this.config.shouldSort || this.isSearching) {
+ choices.sort(filter);
+ }
+
+ choices.forEach((choice) => {
+ const dropdownItem = this._getTemplate('choice', choice);
+ const shouldRender = this.passedElement.type === 'select-one' || !choice.selected;
+ if (shouldRender) {
+ choicesFragment.appendChild(dropdownItem);
+ }
+ });
+
+ return choicesFragment;
+ }
+
+ /**
+ * Render items into a DOM fragment and append to items list
+ * @param {Array} items Items to add to list
+ * @param {DocumentFragment} fragment Fragrment to add items to (optional)
+ * @return
+ * @private
+ */
+ renderItems(items, fragment) {
+ // Create fragment to add elements to
+ const itemListFragment = fragment || document.createDocumentFragment();
+ // Simplify store data to just values
+ const itemsFiltered = this.store.getItemsReducedToValues(items);
+
+ if (this.passedElement.type === 'text') {
+ // Assign hidden input array of values
+ this.passedElement.setAttribute('value', itemsFiltered.join(this.config.delimiter));
+ } else {
+ const selectedOptionsFragment = document.createDocumentFragment();
+
+ // Add each list item to list
+ items.forEach((item) => {
+ // Create a standard select option
+ const option = this._getTemplate('option', item);
+ // Append it to fragment
+ selectedOptionsFragment.appendChild(option);
+ });
+
+ // Update selected choices
+ this.passedElement.innerHTML = '';
+ this.passedElement.appendChild(selectedOptionsFragment);
+ }
+
+ // Add each list item to list
+ items.forEach((item) => {
+ // Create new list element
+ const listItem = this._getTemplate('item', item);
+ // Append it to list
+ itemListFragment.appendChild(listItem);
+ });
+
+ return itemListFragment;
+ }
+
+ /**
+ * Render DOM with values
+ * @return
+ * @private
+ */
+ render() {
+ this.currentState = this.store.getState();
+
+ // Only render if our state has actually changed
+ if (this.currentState !== this.prevState) {
+ // Choices
+ if (this.currentState.choices !== this.prevState.choices || this.currentState.groups !== this.prevState.groups) {
+ if (this.passedElement.type === 'select-multiple' || this.passedElement.type === 'select-one') {
+ // Get active groups/choices
+ const activeGroups = this.store.getGroupsFilteredByActive();
+ const activeChoices = this.store.getChoicesFilteredByActive();
+
+ let choiceListFragment = document.createDocumentFragment();
+
+ // Clear choices
+ this.choiceList.innerHTML = '';
+ // Scroll back to top of choices list
+ this.choiceList.scrollTop = 0;
+
+ // If we have grouped options
+ if (activeGroups.length >= 1 && this.isSearching !== true) {
+ choiceListFragment = this.renderGroups(activeGroups, activeChoices, choiceListFragment);
+ } else if (activeChoices.length >= 1) {
+ choiceListFragment = this.renderChoices(activeChoices, choiceListFragment);
+ }
+
+ if (choiceListFragment.childNodes && choiceListFragment.childNodes.length > 0) {
+ // If we actually have anything to add to our dropdown
+ // append it and highlight the first choice
+ this.choiceList.appendChild(choiceListFragment);
+ this._highlightChoice();
+ } else {
+ // Otherwise show a notice
+ const dropdownItem = this.isSearching ?
+ this._getTemplate('notice', this.config.noResultsText) :
+ this._getTemplate('notice', this.config.noChoicesText);
+ this.choiceList.appendChild(dropdownItem);
+ }
+ }
+ }
+
+ // Items
+ if (this.currentState.items !== this.prevState.items) {
+ const activeItems = this.store.getItemsFilteredByActive();
+ if (activeItems) {
+ // Create a fragment to store our list items
+ // (so we don't have to update the DOM for each item)
+ const itemListFragment = this.renderItems(activeItems);
+
+ // Clear list
+ this.itemList.innerHTML = '';
+
+ // If we have items to add
+ if (itemListFragment.childNodes) {
+ // Update list
+ this.itemList.appendChild(itemListFragment);
+ }
+ }
+ }
+
+ this.prevState = this.currentState;
+ }
+ }
}
window.Choices = module.exports = Choices;
\ No newline at end of file
diff --git a/assets/scripts/src/lib/polyfills.js b/assets/scripts/src/lib/polyfills.js
index ab88f03..f52a5f3 100644
--- a/assets/scripts/src/lib/polyfills.js
+++ b/assets/scripts/src/lib/polyfills.js
@@ -1,113 +1,112 @@
/* eslint-disable */
-
// Production steps of ECMA-262, Edition 6, 22.1.2.1
// Reference: https://people.mozilla.org/~jorendorff/es6-draft.html#sec-array.from
if (!Array.from) {
- Array.from = (function() {
- var toStr = Object.prototype.toString;
+ Array.from = (function() {
+ var toStr = Object.prototype.toString;
- var isCallable = function(fn) {
- return typeof fn === 'function' || toStr.call(fn) === '[object Function]';
- };
+ var isCallable = function(fn) {
+ return typeof fn === 'function' || toStr.call(fn) === '[object Function]';
+ };
- var toInteger = function(value) {
- var number = Number(value);
- if (isNaN(number)) {
- return 0;
- }
- if (number === 0 || !isFinite(number)) {
- return number;
- }
- return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number));
- };
+ var toInteger = function(value) {
+ var number = Number(value);
+ if (isNaN(number)) {
+ return 0;
+ }
+ if (number === 0 || !isFinite(number)) {
+ return number;
+ }
+ return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number));
+ };
- var maxSafeInteger = Math.pow(2, 53) - 1;
+ var maxSafeInteger = Math.pow(2, 53) - 1;
- var toLength = function(value) {
- var len = toInteger(value);
- return Math.min(Math.max(len, 0), maxSafeInteger);
- };
+ var toLength = function(value) {
+ var len = toInteger(value);
+ return Math.min(Math.max(len, 0), maxSafeInteger);
+ };
- // The length property of the from method is 1.
- return function from(arrayLike /*, mapFn, thisArg */ ) {
- // 1. Let C be the this value.
- var C = this;
+ // The length property of the from method is 1.
+ return function from(arrayLike /*, mapFn, thisArg */ ) {
+ // 1. Let C be the this value.
+ var C = this;
- // 2. Let items be ToObject(arrayLike).
- var items = Object(arrayLike);
+ // 2. Let items be ToObject(arrayLike).
+ var items = Object(arrayLike);
- // 3. ReturnIfAbrupt(items).
- if (arrayLike == null) {
- throw new TypeError("Array.from requires an array-like object - not null or undefined");
- }
+ // 3. ReturnIfAbrupt(items).
+ if (arrayLike == null) {
+ throw new TypeError("Array.from requires an array-like object - not null or undefined");
+ }
- // 4. If mapfn is undefined, then let mapping be false.
- var mapFn = arguments.length > 1 ? arguments[1] : void undefined;
- var T;
- if (typeof mapFn !== 'undefined') {
- // 5. else
- // 5. a If IsCallable(mapfn) is false, throw a TypeError exception.
- if (!isCallable(mapFn)) {
- throw new TypeError('Array.from: when provided, the second argument must be a function');
- }
+ // 4. If mapfn is undefined, then let mapping be false.
+ var mapFn = arguments.length > 1 ? arguments[1] : void undefined;
+ var T;
+ if (typeof mapFn !== 'undefined') {
+ // 5. else
+ // 5. a If IsCallable(mapfn) is false, throw a TypeError exception.
+ if (!isCallable(mapFn)) {
+ throw new TypeError('Array.from: when provided, the second argument must be a function');
+ }
- // 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined.
- if (arguments.length > 2) {
- T = arguments[2];
- }
- }
+ // 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined.
+ if (arguments.length > 2) {
+ T = arguments[2];
+ }
+ }
- // 10. Let lenValue be Get(items, "length").
- // 11. Let len be ToLength(lenValue).
- var len = toLength(items.length);
+ // 10. Let lenValue be Get(items, "length").
+ // 11. Let len be ToLength(lenValue).
+ var len = toLength(items.length);
- // 13. If IsConstructor(C) is true, then
- // 13. a. Let A be the result of calling the [[Construct]] internal method of C with an argument list containing the single item len.
- // 14. a. Else, Let A be ArrayCreate(len).
- var A = isCallable(C) ? Object(new C(len)) : new Array(len);
+ // 13. If IsConstructor(C) is true, then
+ // 13. a. Let A be the result of calling the [[Construct]] internal method of C with an argument list containing the single item len.
+ // 14. a. Else, Let A be ArrayCreate(len).
+ var A = isCallable(C) ? Object(new C(len)) : new Array(len);
- // 16. Let k be 0.
- var k = 0;
- // 17. Repeat, while k < len… (also steps a - h)
- var kValue;
- while (k < len) {
- kValue = items[k];
- if (mapFn) {
- A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k);
- } else {
- A[k] = kValue;
- }
- k += 1;
- }
- // 18. Let putStatus be Put(A, "length", len, true).
- A.length = len;
- // 20. Return A.
- return A;
- };
- }());
+ // 16. Let k be 0.
+ var k = 0;
+ // 17. Repeat, while k < len… (also steps a - h)
+ var kValue;
+ while (k < len) {
+ kValue = items[k];
+ if (mapFn) {
+ A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k);
+ } else {
+ A[k] = kValue;
+ }
+ k += 1;
+ }
+ // 18. Let putStatus be Put(A, "length", len, true).
+ A.length = len;
+ // 20. Return A.
+ return A;
+ };
+ }());
}
// Reference: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/find
if (!Array.prototype.find) {
- Array.prototype.find = function(predicate) {
- 'use strict';
- if (this == null) {
- throw new TypeError('Array.prototype.find called on null or undefined');
- }
- if (typeof predicate !== 'function') {
- throw new TypeError('predicate must be a function');
- }
- var list = Object(this);
- var length = list.length >>> 0;
- var thisArg = arguments[1];
- var value;
+ Array.prototype.find = function(predicate) {
+ 'use strict';
+ if (this == null) {
+ throw new TypeError('Array.prototype.find called on null or undefined');
+ }
+ if (typeof predicate !== 'function') {
+ throw new TypeError('predicate must be a function');
+ }
+ var list = Object(this);
+ var length = list.length >>> 0;
+ var thisArg = arguments[1];
+ var value;
- for (var i = 0; i < length; i++) {
- value = list[i];
- if (predicate.call(thisArg, value, i, list)) {
- return value;
- }
- }
- return undefined;
- };
+ for (var i = 0; i < length; i++) {
+ value = list[i];
+ if (predicate.call(thisArg, value, i, list)) {
+ return value;
+ }
+ }
+ return undefined;
+ };
}
\ No newline at end of file
diff --git a/assets/scripts/src/lib/utils.js b/assets/scripts/src/lib/utils.js
index 4468a99..a9d9fa1 100644
--- a/assets/scripts/src/lib/utils.js
+++ b/assets/scripts/src/lib/utils.js
@@ -1,14 +1,13 @@
/* eslint-disable */
-
/**
* Capitalises the first letter of each word in a string
* @param {String} str String to capitalise
* @return {String} Capitalised string
*/
export const capitalise = function(str) {
- return str.replace(/\w\S*/g, function(txt){
- return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
- });
+ return str.replace(/\w\S*/g, function(txt) {
+ return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
+ });
};
/**
@@ -18,8 +17,8 @@ export const capitalise = function(str) {
* @return {Boolean}
*/
export const isType = function(type, obj) {
- var clas = Object.prototype.toString.call(obj).slice(8, -1);
- return obj !== undefined && obj !== null && clas === type;
+ var clas = Object.prototype.toString.call(obj).slice(8, -1);
+ return obj !== undefined && obj !== null && clas === type;
};
/**
@@ -28,10 +27,10 @@ export const isType = function(type, obj) {
* @return {Boolean}
*/
export const isNode = (o) => {
- return (
- typeof Node === "object" ? o instanceof Node :
- o && typeof o === "object" && typeof o.nodeType === "number" && typeof o.nodeName==="string"
- );
+ return (
+ typeof Node === "object" ? o instanceof Node :
+ o && typeof o === "object" && typeof o.nodeType === "number" && typeof o.nodeName === "string"
+ );
};
/**
@@ -40,10 +39,10 @@ export const isNode = (o) => {
* @return {Boolean}
*/
export const isElement = (o) => {
- return (
- typeof HTMLElement === "object" ? o instanceof HTMLElement : //DOM2
- o && typeof o === "object" && o !== null && o.nodeType === 1 && typeof o.nodeName==="string"
- );
+ return (
+ typeof HTMLElement === "object" ? o instanceof HTMLElement : //DOM2
+ o && typeof o === "object" && o !== null && o.nodeType === 1 && typeof o.nodeName === "string"
+ );
};
/**
@@ -52,63 +51,63 @@ export const isElement = (o) => {
* @return {Object} Merged object of arguments
*/
export const extend = function() {
- let extended = {};
- let deep = false;
- let length = arguments.length;
+ let extended = {};
+ let deep = false;
+ let length = arguments.length;
- /**
- * Merge one object into another
- * @param {Object} obj Object to merge into extended object
- */
- let merge = function (obj) {
- for (let prop in obj) {
- if (Object.prototype.hasOwnProperty.call(obj, prop)) {
- // If deep merge and property is an object, merge properties
- if (deep && isType('Object', obj[prop])) {
- extended[prop] = extend( true, extended[prop], obj[prop]);
- } else {
- extended[prop] = obj[prop];
- }
- }
- }
- };
-
- // Loop through each passed argument
- for (let i = 0; i < length; i++) {
- // store argument at position i
- let obj = arguments[i];
-
- // If we are in fact dealing with an object, merge it. Otherwise throw error
- if (isType('Object', obj)) {
- merge(obj);
+ /**
+ * Merge one object into another
+ * @param {Object} obj Object to merge into extended object
+ */
+ let merge = function(obj) {
+ for (let prop in obj) {
+ if (Object.prototype.hasOwnProperty.call(obj, prop)) {
+ // If deep merge and property is an object, merge properties
+ if (deep && isType('Object', obj[prop])) {
+ extended[prop] = extend(true, extended[prop], obj[prop]);
} else {
- console.error('Custom options must be an object');
+ extended[prop] = obj[prop];
}
+ }
}
+ };
- return extended;
+ // Loop through each passed argument
+ for (let i = 0; i < length; i++) {
+ // store argument at position i
+ let obj = arguments[i];
+
+ // If we are in fact dealing with an object, merge it. Otherwise throw error
+ if (isType('Object', obj)) {
+ merge(obj);
+ } else {
+ console.error('Custom options must be an object');
+ }
+ }
+
+ return extended;
};
/**
* CSS transition end event listener
* @return
*/
-export const whichTransitionEvent = function(){
- var t,
+export const whichTransitionEvent = function() {
+ var t,
el = document.createElement("fakeelement");
- var transitions = {
- "transition" : "transitionend",
- "OTransition" : "oTransitionEnd",
- "MozTransition" : "transitionend",
- "WebkitTransition": "webkitTransitionEnd"
- }
+ var transitions = {
+ "transition": "transitionend",
+ "OTransition": "oTransitionEnd",
+ "MozTransition": "transitionend",
+ "WebkitTransition": "webkitTransitionEnd"
+ }
- for (t in transitions){
- if (el.style[t] !== undefined){
- return transitions[t];
- }
+ for (t in transitions) {
+ if (el.style[t] !== undefined) {
+ return transitions[t];
}
+ }
};
/**
@@ -116,21 +115,21 @@ export const whichTransitionEvent = function(){
* @return
*/
export const whichAnimationEvent = function() {
- var t,
- el = document.createElement('fakeelement');
+ var t,
+ el = document.createElement('fakeelement');
- var animations = {
- 'animation': 'animationend',
- 'OAnimation': 'oAnimationEnd',
- 'MozAnimation': 'animationend',
- 'WebkitAnimation': 'webkitAnimationEnd'
- };
+ var animations = {
+ 'animation': 'animationend',
+ 'OAnimation': 'oAnimationEnd',
+ 'MozAnimation': 'animationend',
+ 'WebkitAnimation': 'webkitAnimationEnd'
+ };
- for (t in animations) {
- if (el.style[t] !== undefined) {
- return animations[t];
- }
+ for (t in animations) {
+ if (el.style[t] !== undefined) {
+ return animations[t];
}
+ }
};
/**
@@ -142,103 +141,103 @@ export const whichAnimationEvent = function() {
* @return {Array} Array of parent elements
*/
export const getParentsUntil = function(elem, parent, selector) {
- var parents = [];
- // Get matches
- for (; elem && elem !== document; elem = elem.parentNode) {
+ var parents = [];
+ // Get matches
+ for (; elem && elem !== document; elem = elem.parentNode) {
- // Check if parent has been reached
- if (parent) {
+ // Check if parent has been reached
+ if (parent) {
- var parentType = parent.charAt(0);
-
- // If parent is a class
- if (parentType === '.') {
- if (elem.classList.contains(parent.substr(1))) {
- break;
- }
- }
-
- // If parent is an ID
- if (parentType === '#') {
- if (elem.id === parent.substr(1)) {
- break;
- }
- }
-
- // If parent is a data attribute
- if (parentType === '[') {
- if (elem.hasAttribute(parent.substr(1, parent.length - 1))) {
- break;
- }
- }
-
- // If parent is a tag
- if (elem.tagName.toLowerCase() === parent) {
- break;
- }
+ var parentType = parent.charAt(0);
+ // If parent is a class
+ if (parentType === '.') {
+ if (elem.classList.contains(parent.substr(1))) {
+ break;
}
- if (selector) {
- var selectorType = selector.charAt(0);
+ }
- // If selector is a class
- if (selectorType === '.') {
- if (elem.classList.contains(selector.substr(1))) {
- parents.push(elem);
- }
- }
-
- // If selector is an ID
- if (selectorType === '#') {
- if (elem.id === selector.substr(1)) {
- parents.push(elem);
- }
- }
-
- // If selector is a data attribute
- if (selectorType === '[') {
- if (elem.hasAttribute(selector.substr(1, selector.length - 1))) {
- parents.push(elem);
- }
- }
-
- // If selector is a tag
- if (elem.tagName.toLowerCase() === selector) {
- parents.push(elem);
- }
-
- } else {
- parents.push(elem);
+ // If parent is an ID
+ if (parentType === '#') {
+ if (elem.id === parent.substr(1)) {
+ break;
}
+ }
+
+ // If parent is a data attribute
+ if (parentType === '[') {
+ if (elem.hasAttribute(parent.substr(1, parent.length - 1))) {
+ break;
+ }
+ }
+
+ // If parent is a tag
+ if (elem.tagName.toLowerCase() === parent) {
+ break;
+ }
+
}
+ if (selector) {
+ var selectorType = selector.charAt(0);
+
+ // If selector is a class
+ if (selectorType === '.') {
+ if (elem.classList.contains(selector.substr(1))) {
+ parents.push(elem);
+ }
+ }
+
+ // If selector is an ID
+ if (selectorType === '#') {
+ if (elem.id === selector.substr(1)) {
+ parents.push(elem);
+ }
+ }
+
+ // If selector is a data attribute
+ if (selectorType === '[') {
+ if (elem.hasAttribute(selector.substr(1, selector.length - 1))) {
+ parents.push(elem);
+ }
+ }
+
+ // If selector is a tag
+ if (elem.tagName.toLowerCase() === selector) {
+ parents.push(elem);
+ }
- // Return parents if any exist
- if (parents.length === 0) {
- return null;
} else {
- return parents;
+ parents.push(elem);
}
+ }
+
+ // Return parents if any exist
+ if (parents.length === 0) {
+ return null;
+ } else {
+ return parents;
+ }
};
-export const wrap = function (element, wrapper) {
- wrapper = wrapper || document.createElement('div');
- if (element.nextSibling) {
- element.parentNode.insertBefore(wrapper, element.nextSibling);
- } else {
- element.parentNode.appendChild(wrapper);
- }
- return wrapper.appendChild(element);
+export const wrap = function(element, wrapper) {
+ wrapper = wrapper || document.createElement('div');
+ if (element.nextSibling) {
+ element.parentNode.insertBefore(wrapper, element.nextSibling);
+ } else {
+ element.parentNode.appendChild(wrapper);
+ }
+ return wrapper.appendChild(element);
};
-export const getSiblings = function (elem) {
- var siblings = [];
- var sibling = elem.parentNode.firstChild;
- for ( ; sibling; sibling = sibling.nextSibling ) {
- if ( sibling.nodeType === 1 && sibling !== elem ) {
- siblings.push( sibling );
- }
+export const getSiblings = function(elem) {
+ var siblings = [];
+ var sibling = elem.parentNode.firstChild;
+ for (; sibling; sibling = sibling.nextSibling) {
+ if (sibling.nodeType === 1 && sibling !== elem) {
+ siblings.push(sibling);
}
- return siblings;
+ }
+ return siblings;
};
/**
@@ -248,8 +247,8 @@ export const getSiblings = function (elem) {
* @return {NodeElement} Found parent element
*/
export const findAncestor = function(el, cls) {
- while ((el = el.parentElement) && !el.classList.contains(cls));
- return el;
+ while ((el = el.parentElement) && !el.classList.contains(cls));
+ return el;
};
/**
@@ -260,19 +259,19 @@ export const findAncestor = function(el, cls) {
* @return {Function} A function will be called after it stops being called for a given delay
*/
export const debounce = function(func, wait, immediate) {
- var timeout;
- return function() {
- var context = this,
- args = arguments;
- var later = function() {
- timeout = null;
- if (!immediate) func.apply(context, args);
- };
- var callNow = immediate && !timeout;
- clearTimeout(timeout);
- timeout = setTimeout(later, wait);
- if (callNow) func.apply(context, args);
+ var timeout;
+ return function() {
+ var context = this,
+ args = arguments;
+ var later = function() {
+ timeout = null;
+ if (!immediate) func.apply(context, args);
};
+ var callNow = immediate && !timeout;
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ if (callNow) func.apply(context, args);
+ };
};
/**
@@ -282,14 +281,14 @@ export const debounce = function(func, wait, immediate) {
* @return {Number} Elements Distance from top of page
*/
export const getElemDistance = function(el) {
- var location = 0;
- if (el.offsetParent) {
- do {
- location += el.offsetTop;
- el = el.offsetParent;
- } while (el);
- }
- return location >= 0 ? location : 0;
+ var location = 0;
+ if (el.offsetParent) {
+ do {
+ location += el.offsetTop;
+ el = el.offsetParent;
+ } while (el);
+ }
+ return location >= 0 ? location : 0;
};
/**
@@ -299,11 +298,11 @@ export const getElemDistance = function(el) {
* @return {Number} Height of element
*/
export const getElementOffset = function(el, offset) {
- var elOffset = offset;
- if(elOffset > 1) elOffset = 1;
- if(elOffset > 0) elOffset = 0;
+ var elOffset = offset;
+ if (elOffset > 1) elOffset = 1;
+ if (elOffset > 0) elOffset = 0;
- return Math.max(el.offsetHeight*elOffset);
+ return Math.max(el.offsetHeight * elOffset);
};
/**
@@ -314,15 +313,15 @@ export const getElementOffset = function(el, offset) {
* @return {[HTMLElement} Found element
*/
export const getAdjacentEl = (startEl, className, direction = 1) => {
- if(!startEl || !className) return;
+ if (!startEl || !className) return;
- const parent = startEl.parentNode.parentNode;
- const children = Array.from(parent.querySelectorAll(className));
+ const parent = startEl.parentNode.parentNode;
+ const children = Array.from(parent.querySelectorAll(className));
- const startPos = children.indexOf(startEl);
- const operatorDirection = direction > 0 ? 1 : -1;
+ const startPos = children.indexOf(startEl);
+ const operatorDirection = direction > 0 ? 1 : -1;
- return children[startPos + operatorDirection];
+ return children[startPos + operatorDirection];
};
/**
@@ -331,13 +330,13 @@ export const getAdjacentEl = (startEl, className, direction = 1) => {
* @return {String} Position of scroll
*/
export const getScrollPosition = function(position) {
- if(position === 'bottom') {
- // Scroll position from the bottom of the viewport
- return Math.max((window.scrollY || window.pageYOffset) + (window.innerHeight || document.documentElement.clientHeight));
- } else {
- // Scroll position from the top of the viewport
- return (window.scrollY || window.pageYOffset);
- }
+ if (position === 'bottom') {
+ // Scroll position from the bottom of the viewport
+ return Math.max((window.scrollY || window.pageYOffset) + (window.innerHeight || document.documentElement.clientHeight));
+ } else {
+ // Scroll position from the top of the viewport
+ return (window.scrollY || window.pageYOffset);
+ }
};
/**
@@ -347,8 +346,8 @@ export const getScrollPosition = function(position) {
* @return {Boolean}
*/
export const isInView = function(el, position, offset) {
- // If the user has scrolled further than the distance from the element to the top of its parent
- return this.getScrollPosition(position) > (this.getElemDistance(el) + this.getElementOffset(el, offset)) ? true : false;
+ // If the user has scrolled further than the distance from the element to the top of its parent
+ return this.getScrollPosition(position) > (this.getElemDistance(el) + this.getElementOffset(el, offset)) ? true : false;
};
/**
@@ -359,19 +358,19 @@ export const isInView = function(el, position, offset) {
* @return {Boolean}
*/
export const isScrolledIntoView = (el, parent, direction = 1) => {
- if(!el) return;
+ if (!el) return;
- let isVisible;
+ let isVisible;
- if(direction > 0) {
- // In view from bottom
- isVisible = (parent.scrollTop + parent.offsetHeight) >= (el.offsetTop + el.offsetHeight) ;
- } else {
- // In view from top
- isVisible = el.offsetTop >= parent.scrollTop;
- }
+ if (direction > 0) {
+ // In view from bottom
+ isVisible = (parent.scrollTop + parent.offsetHeight) >= (el.offsetTop + el.offsetHeight);
+ } else {
+ // In view from top
+ isVisible = el.offsetTop >= parent.scrollTop;
+ }
- return isVisible;
+ return isVisible;
};
/**
@@ -380,9 +379,9 @@ export const isScrolledIntoView = (el, parent, direction = 1) => {
* @return {String} Sanitised string
*/
export const stripHTML = function(html) {
- let el = document.createElement("DIV");
- el.innerHTML = html;
- return el.textContent || el.innerText || "";
+ let el = document.createElement("DIV");
+ el.innerHTML = html;
+ return el.textContent || el.innerText || "";
};
/**
@@ -392,15 +391,15 @@ export const stripHTML = function(html) {
* @return
*/
export const addAnimation = (el, animation) => {
- let animationEvent = whichAnimationEvent();
+ let animationEvent = whichAnimationEvent();
- let removeAnimation = () => {
- el.classList.remove(animation);
- el.removeEventListener(animationEvent, removeAnimation, false);
- };
+ let removeAnimation = () => {
+ el.classList.remove(animation);
+ el.removeEventListener(animationEvent, removeAnimation, false);
+ };
- el.classList.add(animation);
- el.addEventListener(animationEvent, removeAnimation, false);
+ el.classList.add(animation);
+ el.addEventListener(animationEvent, removeAnimation, false);
};
@@ -411,7 +410,7 @@ export const addAnimation = (el, animation) => {
* @return {Number} Random number
*/
export const getRandomNumber = function(min, max) {
- return Math.floor(Math.random() * (max - min) + min);
+ return Math.floor(Math.random() * (max - min) + min);
};
/**
@@ -420,18 +419,18 @@ export const getRandomNumber = function(min, max) {
* @return {HTMLElement} Converted node element
*/
export const strToEl = (function() {
- var tmpEl = document.createElement('div');
- return function(str) {
- var r;
- tmpEl.innerHTML = str;
- r = tmpEl.children[0];
+ var tmpEl = document.createElement('div');
+ return function(str) {
+ var r;
+ tmpEl.innerHTML = str;
+ r = tmpEl.children[0];
- while (tmpEl.firstChild) {
- tmpEl.removeChild(tmpEl.firstChild);
- }
+ while (tmpEl.firstChild) {
+ tmpEl.removeChild(tmpEl.firstChild);
+ }
- return r;
- };
+ return r;
+ };
}());
/**
@@ -439,39 +438,39 @@ export const strToEl = (function() {
* @return {Number} Width of input
*/
export const getWidthOfInput = (input) => {
- const value = input.value || input.placeholder;
- let width = input.offsetWidth;
+ const value = input.value || input.placeholder;
+ let width = input.offsetWidth;
- if(value) {
- const testEl = strToEl(`${ value }`);
- testEl.style.position = 'absolute';
- testEl.style.padding = '0';
- testEl.style.top = '-9999px';
- testEl.style.left = '-9999px';
- testEl.style.width = 'auto';
- testEl.style.whiteSpace = 'pre';
+ if (value) {
+ const testEl = strToEl(`${ value }`);
+ testEl.style.position = 'absolute';
+ testEl.style.padding = '0';
+ testEl.style.top = '-9999px';
+ testEl.style.left = '-9999px';
+ testEl.style.width = 'auto';
+ testEl.style.whiteSpace = 'pre';
- document.body.appendChild(testEl);
+ document.body.appendChild(testEl);
- if(value && testEl.offsetWidth !== input.offsetWidth) {
- width = testEl.offsetWidth + 4;
- }
-
- document.body.removeChild(testEl);
+ if (value && testEl.offsetWidth !== input.offsetWidth) {
+ width = testEl.offsetWidth + 4;
}
- return `${width}px`;
+ document.body.removeChild(testEl);
+ }
+
+ return `${width}px`;
};
export const sortByAlpha = (a, b) => {
- const labelA = (a.label || a.value).toLowerCase();
- const labelB = (b.label || b.value).toLowerCase();
+ const labelA = (a.label || a.value).toLowerCase();
+ const labelB = (b.label || b.value).toLowerCase();
- if (labelA < labelB) return -1;
- if (labelA > labelB) return 1;
- return 0;
+ if (labelA < labelB) return -1;
+ if (labelA > labelB) return 1;
+ return 0;
};
export const sortByScore = (a, b) => {
- return a.score - b.score;
+ return a.score - b.score;
};
\ No newline at end of file
diff --git a/assets/scripts/src/reducers/choices.js b/assets/scripts/src/reducers/choices.js
index 3777048..897e670 100644
--- a/assets/scripts/src/reducers/choices.js
+++ b/assets/scripts/src/reducers/choices.js
@@ -1,93 +1,93 @@
const choices = (state = [], action) => {
- switch (action.type) {
- case 'ADD_CHOICE': {
- /*
- A disabled choice appears in the choice dropdown but cannot be selected
- A selected choice has been added to the passed input's value (added as an item)
- An active choice appears within the choice dropdown
- */
- return [...state, {
- id: action.id,
- groupId: action.groupId,
- value: action.value,
- label: action.label,
- disabled: action.disabled,
- selected: false,
- active: true,
- score: 9999,
- }];
- }
-
- case 'ADD_ITEM': {
- let newState = state;
-
- // If all choices need to be activated
- if (action.activateOptions) {
- newState = state.map((choice) => {
- choice.active = action.active;
- return choice;
- });
- }
- // When an item is added and it has an associated choice,
- // we want to disable it so it can't be chosen again
- if (action.choiceId > -1) {
- newState = state.map((choice) => {
- if (choice.id === parseInt(action.choiceId, 10)) {
- choice.selected = true;
- }
- return choice;
- });
- }
-
- return newState;
- }
-
- case 'REMOVE_ITEM': {
- // When an item is removed and it has an associated choice,
- // we want to re-enable it so it can be chosen again
- if (action.choiceId > -1) {
- return state.map((choice) => {
- if (choice.id === parseInt(action.choiceId, 10)) {
- choice.selected = false;
- }
- return choice;
- });
- }
-
- return state;
- }
-
- case 'FILTER_CHOICES': {
- const filteredResults = action.results;
- const filteredState = state.map((choice) => {
- // Set active state based on whether choice is
- // within filtered results
-
- choice.active = filteredResults.some((result) => {
- if (result.item.id === choice.id) {
- choice.score = result.score;
- return true;
- }
- return false;
- });
-
- return choice;
- });
-
- return filteredState;
- }
-
- case 'ACTIVATE_CHOICES': {
- return state.map((choice) => {
- choice.active = action.active;
- return choice;
- });
- }
-
- default: {
- return state;
- }
+ switch (action.type) {
+ case 'ADD_CHOICE': {
+ /*
+ A disabled choice appears in the choice dropdown but cannot be selected
+ A selected choice has been added to the passed input's value (added as an item)
+ An active choice appears within the choice dropdown
+ */
+ return [...state, {
+ id: action.id,
+ groupId: action.groupId,
+ value: action.value,
+ label: action.label,
+ disabled: action.disabled,
+ selected: false,
+ active: true,
+ score: 9999,
+ }];
}
+
+ case 'ADD_ITEM': {
+ let newState = state;
+
+ // If all choices need to be activated
+ if (action.activateOptions) {
+ newState = state.map((choice) => {
+ choice.active = action.active;
+ return choice;
+ });
+ }
+ // When an item is added and it has an associated choice,
+ // we want to disable it so it can't be chosen again
+ if (action.choiceId > -1) {
+ newState = state.map((choice) => {
+ if (choice.id === parseInt(action.choiceId, 10)) {
+ choice.selected = true;
+ }
+ return choice;
+ });
+ }
+
+ return newState;
+ }
+
+ case 'REMOVE_ITEM': {
+ // When an item is removed and it has an associated choice,
+ // we want to re-enable it so it can be chosen again
+ if (action.choiceId > -1) {
+ return state.map((choice) => {
+ if (choice.id === parseInt(action.choiceId, 10)) {
+ choice.selected = false;
+ }
+ return choice;
+ });
+ }
+
+ return state;
+ }
+
+ case 'FILTER_CHOICES': {
+ const filteredResults = action.results;
+ const filteredState = state.map((choice) => {
+ // Set active state based on whether choice is
+ // within filtered results
+
+ choice.active = filteredResults.some((result) => {
+ if (result.item.id === choice.id) {
+ choice.score = result.score;
+ return true;
+ }
+ return false;
+ });
+
+ return choice;
+ });
+
+ return filteredState;
+ }
+
+ case 'ACTIVATE_CHOICES': {
+ return state.map((choice) => {
+ choice.active = action.active;
+ return choice;
+ });
+ }
+
+ default: {
+ return state;
+ }
+ }
};
export default choices;
\ No newline at end of file
diff --git a/assets/scripts/src/reducers/groups.js b/assets/scripts/src/reducers/groups.js
index 29c44df..21b17f4 100644
--- a/assets/scripts/src/reducers/groups.js
+++ b/assets/scripts/src/reducers/groups.js
@@ -1,18 +1,18 @@
const groups = (state = [], action) => {
- switch (action.type) {
- case 'ADD_GROUP': {
- return [...state, {
- id: action.id,
- value: action.value,
- active: action.active,
- disabled: action.disabled,
- }];
- }
-
- default: {
- return state;
- }
+ switch (action.type) {
+ case 'ADD_GROUP': {
+ return [...state, {
+ id: action.id,
+ value: action.value,
+ active: action.active,
+ disabled: action.disabled,
+ }];
}
+
+ default: {
+ return state;
+ }
+ }
};
export default groups;
\ No newline at end of file
diff --git a/assets/scripts/src/reducers/index.js b/assets/scripts/src/reducers/index.js
index 66e6c83..3def812 100644
--- a/assets/scripts/src/reducers/index.js
+++ b/assets/scripts/src/reducers/index.js
@@ -4,22 +4,22 @@ import groups from './groups';
import choices from './choices';
const appReducer = combineReducers({
- items,
- groups,
- choices,
+ items,
+ groups,
+ choices,
});
const rootReducer = (passedState, action) => {
- let state = passedState;
- // If we are clearing all items, groups and options we reassign
- // state and then pass that state to our proper reducer. This isn't
- // mutating our actual state
- // See: http://stackoverflow.com/a/35641992
- if (action.type === 'CLEAR_ALL') {
- state = undefined;
- }
+ let state = passedState;
+ // If we are clearing all items, groups and options we reassign
+ // state and then pass that state to our proper reducer. This isn't
+ // mutating our actual state
+ // See: http://stackoverflow.com/a/35641992
+ if (action.type === 'CLEAR_ALL') {
+ state = undefined;
+ }
- return appReducer(state, action);
+ return appReducer(state, action);
};
export default rootReducer;
\ No newline at end of file
diff --git a/assets/scripts/src/reducers/items.js b/assets/scripts/src/reducers/items.js
index 3789d69..b1d14bc 100644
--- a/assets/scripts/src/reducers/items.js
+++ b/assets/scripts/src/reducers/items.js
@@ -1,47 +1,47 @@
const items = (state = [], action) => {
- switch (action.type) {
- case 'ADD_ITEM': {
- // Add object to items array
- const newState = [...state, {
- id: action.id,
- choiceId: action.choiceId,
- value: action.value,
- label: action.label,
- active: true,
- highlighted: false,
- }];
+ switch (action.type) {
+ case 'ADD_ITEM': {
+ // Add object to items array
+ const newState = [...state, {
+ id: action.id,
+ choiceId: action.choiceId,
+ value: action.value,
+ label: action.label,
+ active: true,
+ highlighted: false,
+ }];
- return newState.map((item) => {
- if (item.highlighted) {
- item.highlighted = false;
- }
- return item;
- });
- }
-
- case 'REMOVE_ITEM': {
- // Set item to inactive
- return state.map((item) => {
- if (item.id === action.id) {
- item.active = false;
- }
- return item;
- });
- }
-
- case 'HIGHLIGHT_ITEM': {
- return state.map((item) => {
- if (item.id === action.id) {
- item.highlighted = action.highlighted;
- }
- return item;
- });
- }
-
- default: {
- return state;
+ return newState.map((item) => {
+ if (item.highlighted) {
+ item.highlighted = false;
}
+ return item;
+ });
}
+
+ case 'REMOVE_ITEM': {
+ // Set item to inactive
+ return state.map((item) => {
+ if (item.id === action.id) {
+ item.active = false;
+ }
+ return item;
+ });
+ }
+
+ case 'HIGHLIGHT_ITEM': {
+ return state.map((item) => {
+ if (item.id === action.id) {
+ item.highlighted = action.highlighted;
+ }
+ return item;
+ });
+ }
+
+ default: {
+ return state;
+ }
+ }
};
export default items;
\ No newline at end of file
diff --git a/assets/scripts/src/store/index.js b/assets/scripts/src/store/index.js
index 648c066..cbb3cd0 100644
--- a/assets/scripts/src/store/index.js
+++ b/assets/scripts/src/store/index.js
@@ -2,149 +2,149 @@ import { createStore } from 'redux';
import rootReducer from './../reducers/index.js';
export default class Store {
- constructor() {
- this.store = createStore(
- rootReducer,
- window.devToolsExtension ? window.devToolsExtension() : undefined
- );
+ constructor() {
+ this.store = createStore(
+ rootReducer
+ , window.devToolsExtension ? window.devToolsExtension() : undefined
+ );
+ }
+
+ /**
+ * Get store object (wrapping Redux method)
+ * @return {Object} State
+ */
+ getState() {
+ return this.store.getState();
+ }
+
+ /**
+ * Dispatch event to store (wrapped Redux method)
+ * @param {Function} action Action function to trigger
+ * @return
+ */
+ dispatch(action) {
+ this.store.dispatch(action);
+ }
+
+ /**
+ * Subscribe store to function call (wrapped Redux method)
+ * @param {Function} onChange Function to trigger when state changes
+ * @return
+ */
+ subscribe(onChange) {
+ this.store.subscribe(onChange);
+ }
+
+ /**
+ * Get items from store
+ * @return {Array} Item objects
+ */
+ getItems() {
+ const state = this.store.getState();
+ return state.items;
+ }
+
+ /**
+ * Get active items from store
+ * @return {Array} Item objects
+ */
+ getItemsFilteredByActive() {
+ const items = this.getItems();
+ const values = items.filter((item) => {
+ return item.active === true;
+ }, []);
+
+ return values;
+ }
+
+ /**
+ * Get items from store reduced to just their values
+ * @return {Array} Item objects
+ */
+ getItemsReducedToValues(items = this.getItems()) {
+ const values = items.reduce((prev, current) => {
+ prev.push(current.value);
+ return prev;
+ }, []);
+
+ return values;
+ }
+
+ /**
+ * Get choices from store
+ * @return {Array} Option objects
+ */
+ getChoices() {
+ const state = this.store.getState();
+ return state.choices;
+ }
+
+ /**
+ * Get active choices from store
+ * @return {Array} Option objects
+ */
+ getChoicesFilteredByActive() {
+ const choices = this.getChoices();
+ const values = choices.filter((choice) => {
+ return choice.active === true;
+ }, []);
+
+ return values;
+ }
+
+ /**
+ * Get selectable choices from store
+ * @return {Array} Option objects
+ */
+ getChoicesFilteredBySelectable() {
+ const choices = this.getChoices();
+ const values = choices.filter((choice) => {
+ return choice.disabled !== true;
+ }, []);
+
+ return values;
+ }
+
+ /**
+ * Get single choice by it's ID
+ * @return {Object} Found choice
+ */
+ getChoiceById(id) {
+ if (id) {
+ const choices = this.getChoicesFilteredByActive();
+ const foundChoice = choices.find((choice) => choice.id === parseInt(id, 10));
+ return foundChoice;
}
+ return false;
+ }
- /**
- * Get store object (wrapping Redux method)
- * @return {Object} State
- */
- getState() {
- return this.store.getState();
- }
+ /**
+ * Get groups from store
+ * @return {Array} Group objects
+ */
+ getGroups() {
+ const state = this.store.getState();
+ return state.groups;
+ }
- /**
- * Dispatch event to store (wrapped Redux method)
- * @param {Function} action Action function to trigger
- * @return
- */
- dispatch(action) {
- this.store.dispatch(action);
- }
+ /**
+ * Get active groups from store
+ * @return {Array} Group objects
+ */
+ getGroupsFilteredByActive() {
+ const groups = this.getGroups();
+ const choices = this.getChoices();
- /**
- * Subscribe store to function call (wrapped Redux method)
- * @param {Function} onChange Function to trigger when state changes
- * @return
- */
- subscribe(onChange) {
- this.store.subscribe(onChange);
- }
+ const values = groups.filter((group) => {
+ const isActive = group.active === true && group.disabled === false;
+ const hasActiveOptions = choices.some((choice) => {
+ return choice.active === true && choice.disabled === false;
+ });
+ return isActive && hasActiveOptions;
+ }, []);
- /**
- * Get items from store
- * @return {Array} Item objects
- */
- getItems() {
- const state = this.store.getState();
- return state.items;
- }
-
- /**
- * Get active items from store
- * @return {Array} Item objects
- */
- getItemsFilteredByActive() {
- const items = this.getItems();
- const values = items.filter((item) => {
- return item.active === true;
- }, []);
-
- return values;
- }
-
- /**
- * Get items from store reduced to just their values
- * @return {Array} Item objects
- */
- getItemsReducedToValues(items = this.getItems()) {
- const values = items.reduce((prev, current) => {
- prev.push(current.value);
- return prev;
- }, []);
-
- return values;
- }
-
- /**
- * Get choices from store
- * @return {Array} Option objects
- */
- getChoices() {
- const state = this.store.getState();
- return state.choices;
- }
-
- /**
- * Get active choices from store
- * @return {Array} Option objects
- */
- getChoicesFilteredByActive() {
- const choices = this.getChoices();
- const values = choices.filter((choice) => {
- return choice.active === true;
- }, []);
-
- return values;
- }
-
- /**
- * Get selectable choices from store
- * @return {Array} Option objects
- */
- getChoicesFilteredBySelectable() {
- const choices = this.getChoices();
- const values = choices.filter((choice) => {
- return choice.disabled !== true;
- }, []);
-
- return values;
- }
-
- /**
- * Get single choice by it's ID
- * @return {Object} Found choice
- */
- getChoiceById(id) {
- if (id) {
- const choices = this.getChoicesFilteredByActive();
- const foundChoice = choices.find((choice) => choice.id === parseInt(id, 10));
- return foundChoice;
- }
- return false;
- }
-
- /**
- * Get groups from store
- * @return {Array} Group objects
- */
- getGroups() {
- const state = this.store.getState();
- return state.groups;
- }
-
- /**
- * Get active groups from store
- * @return {Array} Group objects
- */
- getGroupsFilteredByActive() {
- const groups = this.getGroups();
- const choices = this.getChoices();
-
- const values = groups.filter((group) => {
- const isActive = group.active === true && group.disabled === false;
- const hasActiveOptions = choices.some((choice) => {
- return choice.active === true && choice.disabled === false;
- });
- return isActive && hasActiveOptions;
- }, []);
-
- return values;
- }
+ return values;
+ }
}
module.exports = Store;
\ No newline at end of file