From a325a75a2358f75cbe839db06520a295d2fe2993 Mon Sep 17 00:00:00 2001 From: Josh Johnson Date: Thu, 19 Oct 2017 12:35:26 +0100 Subject: [PATCH] Early returns + refactoring --- src/scripts/src/choices.js | 307 +++++++++++++++++------------------ src/scripts/src/constants.js | 2 + 2 files changed, 152 insertions(+), 157 deletions(-) diff --git a/src/scripts/src/choices.js b/src/scripts/src/choices.js index 8c841a4..7cf5f8a 100644 --- a/src/scripts/src/choices.js +++ b/src/scripts/src/choices.js @@ -6,7 +6,7 @@ import Input from './components/input'; import List from './components/list'; import WrappedInput from './components/wrapped-input'; import WrappedSelect from './components/wrapped-select'; -import { DEFAULT_CONFIG, DEFAULT_CLASSNAMES, EVENTS, KEY_CODES } from './constants'; +import { DEFAULT_CONFIG, DEFAULT_CLASSNAMES, EVENTS, KEY_CODES, SCROLLING_SPEED } from './constants'; import { TEMPLATES } from './templates'; import { addChoice, filterChoices, activateChoices, clearChoices } from './actions/choices'; import { addItem, removeItem, highlightItem } from './actions/items'; @@ -388,105 +388,104 @@ class Choices { */ render() { this.currentState = this.store.getState(); + const stateChanged = ( + this.currentState.choices !== this.prevState.choices || + this.currentState.groups !== this.prevState.groups || + this.currentState.items !== this.prevState.items + ); - // Only render if our state has actually changed - if (this.currentState !== this.prevState) { - const stateChanged = ( - this.currentState.choices !== this.prevState.choices || - this.currentState.groups !== this.prevState.groups || - this.currentState.items !== this.prevState.items - ); - - // Choices - if (stateChanged && this.isSelectElement) { - // Get active groups/choices - const activeGroups = this.store.getGroupsFilteredByActive(); - const activeChoices = this.store.getChoicesFilteredByActive(); - - let choiceListFragment = document.createDocumentFragment(); - - // Clear choices - this.choiceList.clear(); - - // Scroll back to top of choices list - if (this.config.resetScrollPosition) { - this.choiceList.scrollTo(0); - } - - // If we have grouped options - if (activeGroups.length >= 1 && !this.isSearching) { - // If we have a placeholder choice along with groups - const activePlaceholders = activeChoices.filter( - activeChoice => activeChoice.placeholder === true && activeChoice.groupId === -1, - ); - if (activePlaceholders.length >= 1) { - choiceListFragment = this.renderChoices(activePlaceholders, choiceListFragment); - } - choiceListFragment = this.renderGroups(activeGroups, activeChoices, choiceListFragment); - } else if (activeChoices.length >= 1) { - choiceListFragment = this.renderChoices(activeChoices, choiceListFragment); - } - - const activeItems = this.store.getItemsFilteredByActive(); - const canAddItem = this._canAddItem(activeItems, this.input.getValue()); - - // If we have choices to show - if (choiceListFragment.childNodes && choiceListFragment.childNodes.length > 0) { - // ...and we can select them - if (canAddItem.response) { - // ...append them and highlight the first choice - this.choiceList.append(choiceListFragment); - this._highlightChoice(); - } else { - // ...otherwise show a notice - this.choiceList.append(this._getTemplate('notice', canAddItem.notice)); - } - } else { - // Otherwise show a notice - let dropdownItem; - let notice; - - if (this.isSearching) { - notice = isType('Function', this.config.noResultsText) ? - this.config.noResultsText() : - this.config.noResultsText; - - dropdownItem = this._getTemplate('notice', notice, 'no-results'); - } else { - notice = isType('Function', this.config.noChoicesText) ? - this.config.noChoicesText() : - this.config.noChoicesText; - - dropdownItem = this._getTemplate('notice', notice, 'no-choices'); - } - - this.choiceList.append(dropdownItem); - } - } - - // Items - if (stateChanged) { - // Get active items (items that can be selected) - const activeItems = this.store.getItemsFilteredByActive() || []; - - // Clear list - this.itemList.clear(); - - if (activeItems.length) { - // 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); - - // If we have items to add - if (itemListFragment.childNodes) { - // Update list - this.itemList.append(itemListFragment); - } - } - } - - this.prevState = this.currentState; + if (!stateChanged) { + return; } + + /* Choices */ + + if (this.isSelectElement) { + // Get active groups/choices + const activeGroups = this.store.getGroupsFilteredByActive(); + const activeChoices = this.store.getChoicesFilteredByActive(); + + let choiceListFragment = document.createDocumentFragment(); + + // Clear choices + this.choiceList.clear(); + + // Scroll back to top of choices list + if (this.config.resetScrollPosition) { + this.choiceList.scrollTo(0); + } + + // If we have grouped options + if (activeGroups.length >= 1 && !this.isSearching) { + // If we have a placeholder choice along with groups + const activePlaceholders = activeChoices.filter( + activeChoice => activeChoice.placeholder === true && activeChoice.groupId === -1, + ); + if (activePlaceholders.length >= 1) { + choiceListFragment = this.renderChoices(activePlaceholders, choiceListFragment); + } + choiceListFragment = this.renderGroups(activeGroups, activeChoices, choiceListFragment); + } else if (activeChoices.length >= 1) { + choiceListFragment = this.renderChoices(activeChoices, choiceListFragment); + } + + const activeItems = this.store.getItemsFilteredByActive(); + const canAddItem = this._canAddItem(activeItems, this.input.getValue()); + + // If we have choices to show + if (choiceListFragment.childNodes && choiceListFragment.childNodes.length > 0) { + // ...and we can select them + if (canAddItem.response) { + // ...append them and highlight the first choice + this.choiceList.append(choiceListFragment); + this._highlightChoice(); + } else { + // ...otherwise show a notice + this.choiceList.append(this._getTemplate('notice', canAddItem.notice)); + } + } else { + // Otherwise show a notice + let dropdownItem; + let notice; + + if (this.isSearching) { + notice = isType('Function', this.config.noResultsText) ? + this.config.noResultsText() : + this.config.noResultsText; + + dropdownItem = this._getTemplate('notice', notice, 'no-results'); + } else { + notice = isType('Function', this.config.noChoicesText) ? + this.config.noChoicesText() : + this.config.noChoicesText; + + dropdownItem = this._getTemplate('notice', notice, 'no-choices'); + } + + this.choiceList.append(dropdownItem); + } + } + + /* Items */ + + // Get active items (items that can be selected) + const activeItems = this.store.getItemsFilteredByActive() || []; + // Clear list + this.itemList.clear(); + + if (activeItems.length) { + // 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); + + // If we have items to add + if (itemListFragment.childNodes) { + // Update list + this.itemList.append(itemListFragment); + } + } + + this.prevState = this.currentState; } /** @@ -550,10 +549,7 @@ class Choices { eventResponse.groupValue = group.value; } - this.store.dispatch( - highlightItem(id, false), - ); - + this.store.dispatch(highlightItem(id, false)); this.passedElement.triggerEvent(EVENTS.highlightItem, eventResponse); return this; @@ -1141,6 +1137,7 @@ class Choices { this._triggerChange(lastItem.value); } else { if (!hasHighlightedItems) { + // Highlight last item if none already highlighted this.highlightItem(lastItem, false); } this.removeHighlightedItems(true); @@ -1297,26 +1294,26 @@ class Choices { this.currentValue.trim() : this.currentValue; - // If new value matches the desired length and is not the same as the current value with a space - if (newValue.length >= 1 && newValue !== `${currentValue} `) { - const haystack = this.store.getSearchableChoices(); - const needle = newValue; - const keys = isType('Array', this.config.searchFields) ? - this.config.searchFields : - [this.config.searchFields]; - const options = Object.assign(this.config.fuseOptions, { keys }); - const fuse = new Fuse(haystack, options); - const results = fuse.search(needle); - - this.currentValue = newValue; - this.highlightPosition = 0; - this.isSearching = true; - this.store.dispatch(filterChoices(results)); - - return results.length; + if (newValue.length < 1 && newValue === `${currentValue} `) { + return 0; } - return 0; + // If new value matches the desired length and is not the same as the current value with a space + const haystack = this.store.getSearchableChoices(); + const needle = newValue; + const keys = isType('Array', this.config.searchFields) ? + this.config.searchFields : + [this.config.searchFields]; + const options = Object.assign(this.config.fuseOptions, { keys }); + const fuse = new Fuse(haystack, options); + const results = fuse.search(needle); + + this.currentValue = newValue; + this.highlightPosition = 0; + this.isSearching = true; + this.store.dispatch(filterChoices(results)); + + return results.length; } /** @@ -1599,17 +1596,15 @@ class Choices { this.hideDropdown(); } } else { - const backKey = 46; - const deleteKey = 8; + const backKey = KEY_CODES.BACK_KEY; + const deleteKey = KEY_CODES.DELETE_KEY; // 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.isTextElement && this.isSearching) { this.isSearching = false; - this.store.dispatch( - activateChoices(true), - ); + this.store.dispatch(activateChoices(true)); } } else if (this.canSearch && canAddItem.response) { this._handleSearch(this.input.getValue()); @@ -1769,33 +1764,34 @@ class Choices { */ _onFocus(e) { const target = e.target; - // If target is something that concerns us - if (this.containerOuter.element.contains(target)) { - const focusActions = { - text: () => { - if (target === this.input.element) { - this.containerOuter.addFocusState(); - } - }, - 'select-one': () => { - this.containerOuter.addFocusState(); - if (target === this.input.element) { - // Show dropdown if it isn't already showing - this.showDropdown(); - } - }, - 'select-multiple': () => { - if (target === this.input.element) { - // If element is a select box, the focused element is the container and the dropdown - // isn't already open, focus and show dropdown - this.containerOuter.addFocusState(); - this.showDropdown(true); - } - }, - }; - - focusActions[this.passedElement.element.type](); + if (!this.containerOuter.element.contains(target)) { + return; } + + const focusActions = { + text: () => { + if (target === this.input.element) { + this.containerOuter.addFocusState(); + } + }, + 'select-one': () => { + this.containerOuter.addFocusState(); + if (target === this.input.element) { + // Show dropdown if it isn't already showing + this.showDropdown(); + } + }, + 'select-multiple': () => { + if (target === this.input.element) { + // If element is a select box, the focused element is the container and the dropdown + // isn't already open, focus and show dropdown + this.containerOuter.addFocusState(); + this.showDropdown(true); + } + }, + }; + + focusActions[this.passedElement.element.type](); } /** @@ -1882,7 +1878,7 @@ class Choices { choice.offsetTop; const animateScroll = () => { - const strength = 4; + const strength = SCROLLING_SPEED; const choiceListScrollTop = this.choiceList.scrollPos; let continueAnimation = false; let easing; @@ -2068,9 +2064,7 @@ class Choices { const groupId = item.groupId; const group = groupId >= 0 ? this.store.getGroupById(groupId) : null; - this.store.dispatch( - removeItem(id, choiceId), - ); + this.store.dispatch(removeItem(id, choiceId)); if (group && group.value) { this.passedElement.triggerEvent(EVENTS.removeItem, { @@ -2272,7 +2266,6 @@ class Choices { // Wrap input in container preserving DOM ordering wrap(this.passedElement.element, this.containerInner.element); - // Wrapper inner container with outer container wrap(this.containerInner.element, this.containerOuter.element); @@ -2295,7 +2288,7 @@ class Choices { dropdown.appendChild(choiceList); } - if (this.isSelectMultipleElement || this.isTextElement) { + if (!this.isSelectOneElement) { this.containerInner.element.appendChild(this.input.element); } else if (this.canSearch) { dropdown.insertBefore(input, dropdown.firstChild); diff --git a/src/scripts/src/constants.js b/src/scripts/src/constants.js index e0878ad..3b0d88a 100644 --- a/src/scripts/src/constants.js +++ b/src/scripts/src/constants.js @@ -103,3 +103,5 @@ export const KEY_CODES = { PAGE_UP_KEY: 33, PAGE_DOWN_KEY: 34, }; + +export const SCROLLING_SPEED = 4;