From 336b65fef94a209c9ee6aa0bea7a5812e14f294f Mon Sep 17 00:00:00 2001 From: Josh Johnson Date: Mon, 21 Aug 2017 08:53:19 +0100 Subject: [PATCH] Begin moving input to component --- assets/scripts/src/choices.js | 106 ++++++++++++------------- assets/scripts/src/components/input.js | 17 ++++ tests/spec/choices_spec.js | 96 +++++++++++----------- 3 files changed, 116 insertions(+), 103 deletions(-) diff --git a/assets/scripts/src/choices.js b/assets/scripts/src/choices.js index a846bfa..deac6e6 100644 --- a/assets/scripts/src/choices.js +++ b/assets/scripts/src/choices.js @@ -3,6 +3,7 @@ import classNames from 'classnames'; import Store from './store/index'; import Dropdown from './components/dropdown'; import Container from './components/container'; +import Input from './components/input'; import { addItem, removeItem, @@ -516,7 +517,7 @@ class Choices { } const activeItems = this.store.getItemsFilteredByActive(); - const canAddItem = this._canAddItem(activeItems, this.input.value); + const canAddItem = this._canAddItem(activeItems, this.input.element.value); // If we have choices to show if (choiceListFragment.childNodes && choiceListFragment.childNodes.length > 0) { @@ -751,8 +752,8 @@ class Choices { this.dropdown.show(); // Optionally focus the input if we have a search input - if (focusInput && this.canSearch && document.activeElement !== this.input) { - this.input.focus(); + if (focusInput && this.canSearch && document.activeElement !== this.input.element) { + this.input.element.focus(); } triggerEvent(this.passedElement, 'showDropdown', {}); @@ -770,11 +771,11 @@ class Choices { // IE11 ignores aria-label and blocks virtual keyboard // if aria-activedescendant is set without a dropdown - this.input.removeAttribute('aria-activedescendant'); + this.input.element.removeAttribute('aria-activedescendant'); // Optionally blur the input if we have a search input - if (blurInput && this.canSearch && document.activeElement === this.input) { - this.input.blur(); + if (blurInput && this.canSearch && document.activeElement === this.input.element) { + this.input.element.blur(); } triggerEvent(this.passedElement, 'hideDropdown', {}); @@ -1001,13 +1002,8 @@ class Choices { * @public */ clearInput() { - if (this.input.value) { - this.input.value = ''; - } - - if (!this.isSelectOneElement) { - this._setInputWidth(); - } + const shouldSetInputWidth = !this.isSelectOneElement; + this.input.clear(shouldSetInputWidth); if (!this.isTextElement && this.config.searchEnabled) { this.isSearching = false; @@ -1034,7 +1030,7 @@ class Choices { if (isDisabled) { this._addEventListeners(); this.passedElement.removeAttribute('disabled'); - this.input.removeAttribute('disabled'); + this.input.element.removeAttribute('disabled'); this.containerOuter.enable(); } @@ -1057,7 +1053,7 @@ class Choices { if (isEnabled) { this._removeEventListeners(); this.passedElement.setAttribute('disabled', ''); - this.input.setAttribute('disabled', ''); + this.input.element.setAttribute('disabled', ''); this.containerOuter.disable(); } @@ -1183,8 +1179,8 @@ class Choices { // Focus input as without focus, a user cannot do anything with a // highlighted item - if (document.activeElement !== this.input) { - this.input.focus(); + if (document.activeElement !== this.input.element) { + this.input.element.focus(); } } } @@ -1253,7 +1249,7 @@ class Choices { // 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.input.element.value = lastItem.value; this._setInputWidth(); this._removeItem(lastItem); this._triggerChange(lastItem.value); @@ -1343,7 +1339,7 @@ class Choices { placeholderItem.innerHTML = this.config.loadingText; } } else { - this.input.placeholder = this.config.loadingText; + this.input.element.placeholder = this.config.loadingText; } } else { // Remove loading states/text @@ -1352,7 +1348,7 @@ class Choices { if (this.isSelectOneElement) { placeholderItem.innerHTML = (this.placeholder || ''); } else { - this.input.placeholder = (this.placeholder || ''); + this.input.element.placeholder = (this.placeholder || ''); } } } @@ -1455,7 +1451,7 @@ class Choices { const hasUnactiveChoices = choices.some(option => !option.active); // Run callback if it is a function - if (this.input === document.activeElement) { + if (this.input.element === document.activeElement) { // Check that we have a value to search and the input was an alphanumeric character if (value && value.length >= this.config.searchFloor) { let resultCount = 0; @@ -1498,10 +1494,10 @@ class Choices { this.containerOuter.element.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); + this.input.element.addEventListener('input', this._onInput); + this.input.element.addEventListener('paste', this._onPaste); + this.input.element.addEventListener('focus', this._onFocus); + this.input.element.addEventListener('blur', this._onBlur); } /** @@ -1523,10 +1519,10 @@ class Choices { this.containerOuter.element.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); + this.input.element.removeEventListener('input', this._onInput); + this.input.element.removeEventListener('paste', this._onPaste); + this.input.element.removeEventListener('focus', this._onFocus); + this.input.element.removeEventListener('blur', this._onBlur); } /** @@ -1538,12 +1534,12 @@ class Choices { if (this.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. - if (this.input.value && this.input.value.length >= (this.placeholder.length / 1.25)) { - this.input.style.width = getWidthOfInput(this.input); + if (this.input.element.value && this.input.element.value.length >= (this.placeholder.length / 1.25)) { + this.input.element.style.width = getWidthOfInput(this.input.element); } } else { // If there is no placeholder, resize input to contents - this.input.style.width = getWidthOfInput(this.input); + this.input.element.style.width = getWidthOfInput(this.input.element); } } @@ -1553,13 +1549,13 @@ class Choices { * @return */ _onKeyDown(e) { - if (e.target !== this.input && !this.containerOuter.element.contains(e.target)) { + if (e.target !== this.input.element && !this.containerOuter.element.contains(e.target)) { return; } const target = e.target; const activeItems = this.store.getItemsFilteredByActive(); - const hasFocusedInput = this.input === document.activeElement; + const hasFocusedInput = this.input.element === document.activeElement; const hasActiveDropdown = this.dropdown.isActive; const hasItems = this.itemList && this.itemList.children; const keyString = String.fromCharCode(e.keyCode); @@ -1586,7 +1582,7 @@ class Choices { // 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) { + if (this.config.removeItems && !this.input.element.value && this.input.element === document.activeElement) { // Highlight items this.highlightAll(); } @@ -1596,7 +1592,7 @@ class Choices { const onEnterKey = () => { // If enter key is pressed and the input has a value if (this.isTextElement && target.value) { - const value = this.input.value; + const value = this.input.element.value; const canAddItem = this._canAddItem(activeItems, value); // All is good, add @@ -1719,11 +1715,11 @@ class Choices { * @private */ _onKeyUp(e) { - if (e.target !== this.input) { + if (e.target !== this.input.element) { return; } - const value = this.input.value; + const value = this.input.element.value; const activeItems = this.store.getItemsFilteredByActive(); const canAddItem = this._canAddItem(activeItems, value); @@ -1762,7 +1758,7 @@ class Choices { ); } } else if (this.canSearch && canAddItem.response) { - this._handleSearch(this.input.value); + this._handleSearch(this.input.element.value); } } // Re-establish canSearch value from changes in _onKeyDown @@ -1810,8 +1806,8 @@ class Choices { ) { if (this.isTextElement) { // If text element, we only want to focus the input (if it isn't already) - if (document.activeElement !== this.input) { - this.input.focus(); + if (document.activeElement !== this.input.element) { + this.input.element.focus(); } } else if (!hasActiveDropdown) { // If a select box, we want to show the dropdown @@ -1839,7 +1835,7 @@ class Choices { this.isScrollingOnIe = true; } - if (this.containerOuter.element.contains(target) && target !== this.input) { + if (this.containerOuter.element.contains(target) && target !== this.input.element) { const activeItems = this.store.getItemsFilteredByActive(); const hasShiftKey = e.shiftKey; @@ -1879,8 +1875,8 @@ class Choices { if (!hasActiveDropdown) { if (this.isTextElement) { - if (document.activeElement !== this.input) { - this.input.focus(); + if (document.activeElement !== this.input.element) { + this.input.element.focus(); } } else if (this.canSearch) { this.showDropdown(true); @@ -1890,7 +1886,7 @@ class Choices { } } else if ( this.isSelectOneElement && - target !== this.input && + target !== this.input.element && !this.dropdown.element.contains(target) ) { this.hideDropdown(true); @@ -1937,7 +1933,7 @@ class Choices { */ _onPaste(e) { // Disable pasting into the input if option has been set - if (e.target === this.input && !this.config.paste) { + if (e.target === this.input.element && !this.config.paste) { e.preventDefault(); } } @@ -1955,13 +1951,13 @@ class Choices { const hasActiveDropdown = this.dropdown.isActive; const focusActions = { text: () => { - if (target === this.input) { + if (target === this.input.element) { this.containerOuter.focus(); } }, 'select-one': () => { this.containerOuter.focus(); - if (target === this.input) { + if (target === this.input.element) { // Show dropdown if it isn't already showing if (!hasActiveDropdown) { this.showDropdown(); @@ -1969,7 +1965,7 @@ class Choices { } }, 'select-multiple': () => { - if (target === this.input) { + 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.focus(); @@ -2000,7 +1996,7 @@ class Choices { const hasHighlightedItems = activeItems.some(item => item.highlighted); const blurActions = { text: () => { - if (target === this.input) { + if (target === this.input.element) { // Remove the focus state this.containerOuter.blur(); // De-select any highlighted items @@ -2021,13 +2017,13 @@ class Choices { this.hideDropdown(); } } - if (target === this.input && hasActiveDropdown) { + if (target === this.input.element && hasActiveDropdown) { // Hide dropdown if it is showing this.hideDropdown(); } }, 'select-multiple': () => { - if (target === this.input) { + if (target === this.input.element) { // Remove the focus state this.containerOuter.blur(); // Hide dropdown if it is showing @@ -2048,7 +2044,7 @@ class Choices { // closes the dropdown. To stop this, we refocus our input // if we know we are on IE *and* are scrolling. this.isScrollingOnIe = false; - this.input.focus(); + this.input.element.focus(); } } @@ -2159,7 +2155,7 @@ class Choices { if (hasActiveDropdown) { // IE11 ignores aria-label and blocks virtual keyboard // if aria-activedescendant is set without a dropdown - this.input.setAttribute('aria-activedescendant', passedEl.id); + this.input.element.setAttribute('aria-activedescendant', passedEl.id); this.containerOuter.element.setAttribute('aria-activedescendant', passedEl.id); } } @@ -2668,7 +2664,7 @@ class Choices { this.containerOuter = new Container(this, containerOuter); this.containerInner = new Container(this, containerInner); - this.input = input; + this.input = new Input(this, input); this.choiceList = choiceList; this.itemList = itemList; this.dropdown = new Dropdown(this, dropdown, this.config.classNames); diff --git a/assets/scripts/src/components/input.js b/assets/scripts/src/components/input.js index 7822003..c5db52a 100644 --- a/assets/scripts/src/components/input.js +++ b/assets/scripts/src/components/input.js @@ -7,4 +7,21 @@ export default class Input { this.element = element; this.classNames = classNames; } + + /** + * Set value of input to blank + * @return {Object} Class instance + * @public + */ + clear(setWidth = true) { + if (this.element.value) { + this.element.value = ''; + } + + if (setWidth) { + this._setInputWidth(); + } + + return this.instance; + } } diff --git a/tests/spec/choices_spec.js b/tests/spec/choices_spec.js index dae9022..590a822 100644 --- a/tests/spec/choices_spec.js +++ b/tests/spec/choices_spec.js @@ -131,7 +131,7 @@ describe('Choices', () => { }); it('should create an input', function() { - expect(this.choices.input).toEqual(jasmine.any(HTMLElement)); + expect(this.choices.input.element).toEqual(jasmine.any(HTMLElement)); }); it('should create a dropdown', function() { @@ -173,33 +173,33 @@ describe('Choices', () => { it('should apply placeholderValue to input', function() { this.choices = new Choices(this.input); - expect(this.choices.input.placeholder).toEqual('Placeholder text'); + expect(this.choices.input.element.placeholder).toEqual('Placeholder text'); }); it('should not apply searchPlaceholderValue to input', function() { this.choices = new Choices(this.input); - expect(this.choices.input.placeholder).not.toEqual('Test'); + expect(this.choices.input.element.placeholder).not.toEqual('Test'); }); it('should accept a user inputted value', function() { this.choices = new Choices(this.input); - this.choices.input.focus(); - this.choices.input.value = 'test'; + this.choices.input.element.focus(); + this.choices.input.element.value = 'test'; this.choices._onKeyDown({ - target: this.choices.input, + target: this.choices.input.element, keyCode: 13, ctrlKey: false, }); - expect(this.choices.currentState.items[0].value).toContain(this.choices.input.value); + expect(this.choices.currentState.items[0].value).toContain(this.choices.input.element.value); }); it('should copy the passed placeholder to the cloned input', function() { this.choices = new Choices(this.input); - expect(this.choices.input.placeholder).toEqual(this.input.placeholder); + expect(this.choices.input.element.placeholder).toEqual(this.input.placeholder); }); it('should not allow duplicates if duplicateItems is false', function() { @@ -208,16 +208,16 @@ describe('Choices', () => { items: ['test 1'], }); - this.choices.input.focus(); - this.choices.input.value = 'test 1'; + this.choices.input.element.focus(); + this.choices.input.element.value = 'test 1'; this.choices._onKeyDown({ - target: this.choices.input, + target: this.choices.input.element, keyCode: 13, ctrlKey: false, }); - expect(this.choices.currentState.items[this.choices.currentState.items.length - 1]).not.toContain(this.choices.input.value); + expect(this.choices.currentState.items[this.choices.currentState.items.length - 1]).not.toContain(this.choices.input.element.value); }); it('should filter input if regexFilter is passed', function() { @@ -225,20 +225,20 @@ describe('Choices', () => { regexFilter: /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, }); - this.choices.input.focus(); - this.choices.input.value = 'josh@joshuajohnson.co.uk'; + this.choices.input.element.focus(); + this.choices.input.element.value = 'josh@joshuajohnson.co.uk'; this.choices._onKeyDown({ - target: this.choices.input, + target: this.choices.input.element, keyCode: 13, ctrlKey: false, }); - this.choices.input.focus(); - this.choices.input.value = 'not an email address'; + this.choices.input.element.focus(); + this.choices.input.element.value = 'not an email address'; this.choices._onKeyDown({ - target: this.choices.input, + target: this.choices.input.element, keyCode: 13, ctrlKey: false, }); @@ -255,8 +255,8 @@ describe('Choices', () => { appendValue: '-value', }); - this.choices.input.focus(); - this.choices.input.value = 'test'; + this.choices.input.element.focus(); + this.choices.input.element.value = 'test'; this.choices._onKeyDown({ target: this.choices.input, @@ -298,7 +298,7 @@ describe('Choices', () => { placeholderValue: 'Placeholder', }); - expect(this.choices.input.placeholder).not.toEqual('Placeholder'); + expect(this.choices.input.element.placeholder).not.toEqual('Placeholder'); }); it('should apply searchPlaceholderValue to input', function() { @@ -306,12 +306,12 @@ describe('Choices', () => { searchPlaceholderValue: 'Placeholder', }); - expect(this.choices.input.placeholder).toEqual('Placeholder'); + expect(this.choices.input.element.placeholder).toEqual('Placeholder'); }); it('should open the choice list on focusing', function() { this.choices = new Choices(this.input); - this.choices.input.focus(); + this.choices.input.element.focus(); expect(this.choices.dropdown.element.classList).toContain(this.choices.config.classNames.activeState); }); @@ -324,7 +324,7 @@ describe('Choices', () => { this.choices = new Choices(this.input, { renderChoiceLimit: -1, }); - this.choices.input.focus(); + this.choices.input.element.focus(); for (let i = 0; i < 2; i++) { // Key down to third choice @@ -341,7 +341,7 @@ describe('Choices', () => { it('should select choice on enter key press', function() { this.choices = new Choices(this.input); - this.choices.input.focus(); + this.choices.input.element.focus(); // Key down to second choice this.choices._onKeyDown({ @@ -353,7 +353,7 @@ describe('Choices', () => { // Key down to select choice this.choices._onKeyDown({ - target: this.choices.input, + target: this.choices.input.element, keyCode: 13, ctrlKey: false, preventDefault: () => {}, @@ -372,11 +372,11 @@ describe('Choices', () => { passedElement.addEventListener('change', changeSpy); passedElement.addEventListener('addItem', addSpy); - this.choices.input.focus(); + this.choices.input.element.focus(); // Key down to second choice this.choices._onKeyDown({ - target: this.choices.input, + target: this.choices.input.element, keyCode: 40, ctrlKey: false, preventDefault: () => {}, @@ -384,7 +384,7 @@ describe('Choices', () => { // Key down to select choice this.choices._onKeyDown({ - target: this.choices.input, + target: this.choices.input.element, keyCode: 13, ctrlKey: false, preventDefault: () => {}, @@ -464,7 +464,7 @@ describe('Choices', () => { passedElement.addEventListener('showDropdown', showDropdownSpy); - this.choices.input.focus(); + this.choices.input.element.focus(); this.choices._onClick({ target: container, @@ -484,7 +484,7 @@ describe('Choices', () => { passedElement.addEventListener('hideDropdown', hideDropdownSpy); - this.choices.input.focus(); + this.choices.input.element.focus(); this.choices._onClick({ target: container, @@ -509,17 +509,17 @@ describe('Choices', () => { passedElement.addEventListener('search', searchSpy); - this.choices.input.focus(); - this.choices.input.value = '3 '; + this.choices.input.element.focus(); + this.choices.input.element.value = '3 '; // Key down to search this.choices._onKeyUp({ - target: this.choices.input, + target: this.choices.input.element, keyCode: 13, ctrlKey: false, }); - const mostAccurateResult = this.choices.currentState.choices.filter((choice) => choice.active); + const mostAccurateResult = this.choices.currentState.choices.filter(choice => choice.active); expect(this.choices.isSearching && mostAccurateResult[0].value === 'Value 3').toBe(true); expect(searchSpy).toHaveBeenCalled(); @@ -537,17 +537,17 @@ describe('Choices', () => { passedElement.addEventListener('search', searchSpy); - this.choices.input.focus(); - this.choices.input.value = 'Javascript'; + this.choices.input.element.focus(); + this.choices.input.element.value = 'Javascript'; // Key down to search this.choices._onKeyUp({ - target: this.choices.input, + target: this.choices.input.element, keyCode: 13, ctrlKey: false, }); - const activeOptions = this.choices.currentState.choices.filter((choice) => choice.active); + const activeOptions = this.choices.currentState.choices.filter(choice => choice.active); expect(activeOptions.length).toEqual(this.choices.currentState.choices.length); expect(searchSpy).toHaveBeenCalled(); @@ -595,7 +595,7 @@ describe('Choices', () => { searchPlaceholderValue: dummyPlaceholder, }); - expect(this.choices.input.placeholder).toEqual(dummyPlaceholder); + expect(this.choices.input.element.placeholder).toEqual(dummyPlaceholder); }); }); @@ -644,11 +644,11 @@ describe('Choices', () => { }); it('should apply placeholderValue to input', function() { - expect(this.choices.input.placeholder).toEqual('Placeholder text'); + expect(this.choices.input.element.placeholder).toEqual('Placeholder text'); }); it('should not apply searchPlaceholderValue to input', function() { - expect(this.choices.input.placeholder).not.toEqual('Test'); + expect(this.choices.input.element.placeholder).not.toEqual('Test'); }); it('should add any pre-defined values', function() { @@ -660,7 +660,7 @@ describe('Choices', () => { }); it('should add a placeholder defined in the config to the search input', function() { - expect(this.choices.input.placeholder).toEqual('Placeholder text'); + expect(this.choices.input.element.placeholder).toEqual('Placeholder text'); }); }); @@ -872,7 +872,7 @@ describe('Choices', () => { it('should handle disable()', function() { this.choices.disable(); - expect(this.choices.input.disabled).toBe(true); + expect(this.choices.input.element.disabled).toBe(true); expect(this.choices.containerOuter.element.classList.contains(this.choices.config.classNames.disabledState)).toBe(true); expect(this.choices.containerOuter.element.getAttribute('aria-disabled')).toBe('true'); }); @@ -880,7 +880,7 @@ describe('Choices', () => { it('should handle enable()', function() { this.choices.enable(); - expect(this.choices.input.disabled).toBe(false); + expect(this.choices.input.element.disabled).toBe(false); expect(this.choices.containerOuter.element.classList.contains(this.choices.config.classNames.disabledState)).toBe(false); expect(this.choices.containerOuter.element.hasAttribute('aria-disabled')).toBe(false); }); @@ -961,7 +961,7 @@ describe('Choices', () => { it('should handle clearInput()', function() { this.choices.clearInput(); - expect(this.choices.input.value).toBe(''); + expect(this.choices.input.element.value).toBe(''); }); it('should handle removeItemsByValue()', function() { @@ -1005,7 +1005,7 @@ describe('Choices', () => { }); const container = this.choices.containerOuter.element; - this.choices.input.focus(); + this.choices.input.element.focus(); expect(container.classList.contains(this.choices.config.classNames.flippedState)).toBe(true); }); @@ -1015,7 +1015,7 @@ describe('Choices', () => { }); const container = this.choices.containerOuter.element; - this.choices.input.focus(); + this.choices.input.element.focus(); expect(container.classList.contains(this.choices.config.classNames.flippedState)).toBe(false); });