From 809b62c60d1b3ad1d8c3908bcc3398a35c8d47a4 Mon Sep 17 00:00:00 2001 From: Josh Johnson Date: Tue, 29 Aug 2017 12:56:54 +0100 Subject: [PATCH] Add remaining components --- assets/scripts/src/choices.js | 41 ++++++++++---------- assets/scripts/src/components/container.js | 45 ++++++++++++++++++++-- assets/scripts/src/components/dropdown.js | 20 ++++++---- assets/scripts/src/components/input.js | 15 +++++--- assets/scripts/src/components/list.js | 39 +++++++++++++++++++ tests/spec/choices_spec.js | 16 ++++---- 6 files changed, 132 insertions(+), 44 deletions(-) create mode 100644 assets/scripts/src/components/list.js diff --git a/assets/scripts/src/choices.js b/assets/scripts/src/choices.js index 44f628e..cc7cd03 100644 --- a/assets/scripts/src/choices.js +++ b/assets/scripts/src/choices.js @@ -4,6 +4,7 @@ import Store from './store/index'; import Dropdown from './components/dropdown'; import Container from './components/container'; import Input from './components/input'; +import List from './components/list'; import { addItem, removeItem, @@ -496,11 +497,11 @@ class Choices { let choiceListFragment = document.createDocumentFragment(); // Clear choices - this.choiceList.innerHTML = ''; + this.choiceList.clear(); // Scroll back to top of choices list if (this.config.resetScrollPosition) { - this.choiceList.scrollTop = 0; + this.choiceList.scrollTo(0); } // If we have grouped options @@ -518,11 +519,11 @@ class Choices { // ...and we can select them if (canAddItem.response) { // ...append them and highlight the first choice - this.choiceList.appendChild(choiceListFragment); + this.choiceList.append(choiceListFragment); this._highlightChoice(); } else { // ...otherwise show a notice - this.choiceList.appendChild(this._getTemplate('notice', canAddItem.notice)); + this.choiceList.append(this._getTemplate('notice', canAddItem.notice)); } } else { // Otherwise show a notice @@ -543,7 +544,7 @@ class Choices { dropdownItem = this._getTemplate('notice', notice, 'no-choices'); } - this.choiceList.appendChild(dropdownItem); + this.choiceList.append(dropdownItem); } } } @@ -554,7 +555,7 @@ class Choices { const activeItems = this.store.getItemsFilteredByActive(); // Clear list - this.itemList.innerHTML = ''; + this.itemList.clear(); if (activeItems && activeItems) { // Create a fragment to store our list items @@ -564,7 +565,7 @@ class Choices { // If we have items to add if (itemListFragment.childNodes) { // Update list - this.itemList.appendChild(itemListFragment); + this.itemList.append(itemListFragment); } } } @@ -742,7 +743,7 @@ class Choices { * @public */ showDropdown(focusInput = false) { - this.containerOuter.open(this.dropdown.getPosition()); + this.containerOuter.open(this.dropdown.getVerticalPos()); this.dropdown.show(); this.input.activate(focusInput); @@ -1308,13 +1309,13 @@ class Choices { * @private */ _handleLoadingState(isLoading = true) { - let placeholderItem = this.itemList.querySelector(`.${this.config.classNames.placeholder}`); + let placeholderItem = this.itemList.getChild(`.${this.config.classNames.placeholder}`); if (isLoading) { this.containerOuter.addLoadingState(); if (this.isSelectOneElement) { if (!placeholderItem) { placeholderItem = this._getTemplate('placeholder', this.config.loadingText); - this.itemList.appendChild(placeholderItem); + this.itemList.append(placeholderItem); } else { placeholderItem.innerHTML = this.config.loadingText; } @@ -1520,7 +1521,7 @@ class Choices { const activeItems = this.store.getItemsFilteredByActive(); const hasFocusedInput = this.input.isFocussed; const hasActiveDropdown = this.dropdown.isActive; - const hasItems = this.itemList && this.itemList.children; + const hasItems = this.itemList.hasChildren; const keyString = String.fromCharCode(e.keyCode); const backKey = 46; @@ -1580,7 +1581,7 @@ class Choices { if (hasActiveDropdown) { e.preventDefault(); - const highlighted = this.dropdown.getHighlightedChildren(); + const highlighted = this.dropdown.getChild(`.${this.config.classNames.highlightedState}`); // If we have a highlighted choice if (highlighted) { @@ -1997,20 +1998,20 @@ class Choices { return; } - const dropdownHeight = this.choiceList.offsetHeight; + const dropdownHeight = this.choiceList.height; 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; + const containerScrollPos = this.choiceList.scrollPos + dropdownHeight; // Difference between the choice and scroll position const endPoint = direction > 0 ? ( - (this.choiceList.scrollTop + choicePos) - containerScrollPos) : + (this.choiceList.scrollPos + choicePos) - containerScrollPos) : choice.offsetTop; const animateScroll = () => { const strength = 4; - const choiceListScrollTop = this.choiceList.scrollTop; + const choiceListScrollTop = this.choiceList.scrollPos; let continueAnimation = false; let easing; let distance; @@ -2019,7 +2020,7 @@ class Choices { easing = (endPoint - choiceListScrollTop) / strength; distance = easing > 1 ? easing : 1; - this.choiceList.scrollTop = choiceListScrollTop + distance; + this.choiceList.scrollTo(choiceListScrollTop + distance); if (choiceListScrollTop < endPoint) { continueAnimation = true; } @@ -2027,7 +2028,7 @@ class Choices { easing = (choiceListScrollTop - endPoint) / strength; distance = easing > 1 ? easing : 1; - this.choiceList.scrollTop = choiceListScrollTop - distance; + this.choiceList.scrollTo(choiceListScrollTop - distance); if (choiceListScrollTop > endPoint) { continueAnimation = true; } @@ -2604,8 +2605,8 @@ class Choices { this.containerOuter = new Container(this, containerOuter); this.containerInner = new Container(this, containerInner); this.input = new Input(this, input); - this.choiceList = choiceList; - this.itemList = itemList; + this.choiceList = new List(this, choiceList); + this.itemList = new List(this, itemList); this.dropdown = new Dropdown(this, dropdown, this.config.classNames); // Hide passed input diff --git a/assets/scripts/src/components/container.js b/assets/scripts/src/components/container.js index f4197b6..b5689f1 100644 --- a/assets/scripts/src/components/container.js +++ b/assets/scripts/src/components/container.js @@ -1,6 +1,3 @@ -/** - * Container - */ export default class Container { constructor(instance, element) { this.instance = instance; @@ -11,28 +8,49 @@ export default class Container { this.isFlipped = false; this.isFocussed = false; this.isDisabled = false; + this.isLoading = false; this.onFocus = this.onFocus.bind(this); this.onBlur = this.onBlur.bind(this); } + /** + * Add event listeners + */ addEventListeners() { this.element.addEventListener('focus', this.onFocus); this.element.addEventListener('blur', this.onBlur); } + /** + * Remove event listeners + */ + + /** */ removeEventListeners() { this.element.removeEventListener('focus', this.onFocus); this.element.removeEventListener('blur', this.onBlur); } + /** + * Set focussed state + */ onFocus() { this.isFocussed = true; } + /** + * Remove blurred state + */ onBlur() { this.isFocussed = false; } + /** + * Determine whether container should be flipped + * based on passed dropdown position + * @param {Number} dropdownPos + * @returns + */ shouldFlip(dropdownPos) { if (!dropdownPos) { return false; @@ -60,10 +78,17 @@ export default class Container { return shouldFlip; } + /** + * Set active descendant attribute + * @param {Number} activeDescendant ID of active descendant + */ setActiveDescendant(activeDescendant) { this.element.setAttribute('aria-activedescendant', activeDescendant); } + /** + * Remove active descendant attribute + */ removeActiveDescendant() { this.element.removeAttribute('aria-activedescendant'); } @@ -106,6 +131,9 @@ export default class Container { this.element.classList.remove(this.classNames.focusState); } + /** + * Remove disabled state + */ enable() { this.element.classList.remove(this.config.classNames.disabledState); this.element.removeAttribute('aria-disabled'); @@ -115,6 +143,9 @@ export default class Container { this.isDisabled = false; } + /** + * Set disabled state + */ disable() { this.element.classList.add(this.config.classNames.disabledState); this.element.setAttribute('aria-disabled', 'true'); @@ -124,13 +155,21 @@ export default class Container { this.isDisabled = true; } + /** + * Add loading state to element + */ addLoadingState() { this.element.classList.add(this.classNames.loadingState); this.element.setAttribute('aria-busy', 'true'); + this.isLoading = true; } + /** + * Remove loading state from element + */ removeLoadingState() { this.element.classList.remove(this.classNames.loadingState); this.element.removeAttribute('aria-busy'); + this.isLoading = false; } } diff --git a/assets/scripts/src/components/dropdown.js b/assets/scripts/src/components/dropdown.js index f94519b..bed244e 100644 --- a/assets/scripts/src/components/dropdown.js +++ b/assets/scripts/src/components/dropdown.js @@ -1,6 +1,3 @@ -/** - * Dropdown - */ export default class Dropdown { constructor(instance, element, classNames) { this.instance = instance; @@ -11,16 +8,23 @@ export default class Dropdown { this.isActive = false; } - getPosition() { + /** + * Determine how far the top of our element is from + * the top of the window + * @return {Number} Vertical position + */ + getVerticalPos() { this.dimensions = this.element.getBoundingClientRect(); this.position = Math.ceil(this.dimensions.top + window.scrollY + this.element.offsetHeight); return this.position; } - getHighlightedChildren() { - return this.element.querySelector( - `.${this.classNames.highlightedState}`, - ); + /** + * Find element that matches passed selector + * @return {HTMLElement} + */ + getChild(selector) { + return this.element.querySelector(selector); } /** diff --git a/assets/scripts/src/components/input.js b/assets/scripts/src/components/input.js index ed05fed..fcab558 100644 --- a/assets/scripts/src/components/input.js +++ b/assets/scripts/src/components/input.js @@ -1,14 +1,11 @@ import { getWidthOfInput } from '../lib/utils'; -/** - * Input - */ export default class Input { - constructor(instance, element, classNames) { + constructor(instance, element) { this.instance = instance; this.element = element; - this.classNames = classNames; this.isFocussed = this.element === document.activeElement; + this.isDisabled = false; // Bind event listeners this.onPaste = this.onPaste.bind(this); @@ -55,10 +52,16 @@ export default class Input { } } + /** + * Set focussed state + */ onFocus() { this.isFocussed = true; } + /** + * Remove focussed state + */ onBlur() { this.isFocussed = false; } @@ -80,10 +83,12 @@ export default class Input { enable() { this.element.removeAttribute('disabled'); + this.isDisabled = false; } disable() { this.element.setAttribute('disabled', ''); + this.isDisabled = true; } focus() { diff --git a/assets/scripts/src/components/list.js b/assets/scripts/src/components/list.js new file mode 100644 index 0000000..184347a --- /dev/null +++ b/assets/scripts/src/components/list.js @@ -0,0 +1,39 @@ +export default class List { + constructor(instance, element) { + this.instance = instance; + this.element = element; + this.classNames = this.instance.config.classNames; + this.scrollPos = this.element.scrollTop; + this.height = this.element.offsetHeight; + this.hasChildren = !!this.element.children; + } + + /** + * Clear List contents + */ + clear() { + this.element.innerHTML = ''; + } + + /** + * Scroll to passed position on Y axis + */ + scrollTo(scrollPos) { + this.element.scrollTop = scrollPos; + } + + /** + * Append node to element + */ + append(node) { + this.element.appendChild(node); + } + + /** + * Find element that matches passed selector + * @return {HTMLElement} + */ + getChild(selector) { + return this.element.querySelector(selector); + } +} diff --git a/tests/spec/choices_spec.js b/tests/spec/choices_spec.js index 61f4d05..97bd3e8 100644 --- a/tests/spec/choices_spec.js +++ b/tests/spec/choices_spec.js @@ -124,11 +124,11 @@ describe('Choices', () => { }); it('should create a choice list', function() { - expect(this.choices.choiceList).toEqual(jasmine.any(HTMLElement)); + expect(this.choices.choiceList.element).toEqual(jasmine.any(HTMLElement)); }); it('should create an item list', function() { - expect(this.choices.itemList).toEqual(jasmine.any(HTMLElement)); + expect(this.choices.itemList.element).toEqual(jasmine.any(HTMLElement)); }); it('should create an input', function() { @@ -219,7 +219,7 @@ describe('Choices', () => { }); expect( - this.choices.currentState.items[this.choices.currentState.items.length - 1] + this.choices.currentState.items[this.choices.currentState.items.length - 1], ).not.toContain(this.choices.input.element.value); }); @@ -435,7 +435,7 @@ describe('Choices', () => { expect( document.activeElement === this.choices.input.element && - container.classList.contains(openState) + container.classList.contains(openState), ).toBe(false); }); @@ -896,7 +896,7 @@ describe('Choices', () => { expect(this.choices.input.element.disabled).toBe(true); expect( this.choices.containerOuter.element.classList.contains( - this.choices.config.classNames.disabledState + this.choices.config.classNames.disabledState, ), ).toBe(true); expect(this.choices.containerOuter.element.getAttribute('aria-disabled')).toBe('true'); @@ -1053,7 +1053,7 @@ describe('Choices', () => { renderSelectedChoices: 'always', renderChoiceLimit: -1, }); - const renderedChoices = this.choices.choiceList.querySelectorAll('.choices__item'); + const renderedChoices = this.choices.choiceList.element.querySelectorAll('.choices__item'); expect(renderedChoices.length).toEqual(3); }); @@ -1062,7 +1062,7 @@ describe('Choices', () => { renderSelectedChoices: 'auto', renderChoiceLimit: -1, }); - const renderedChoices = this.choices.choiceList.querySelectorAll('.choices__item'); + const renderedChoices = this.choices.choiceList.element.querySelectorAll('.choices__item'); expect(renderedChoices.length).toEqual(1); }); @@ -1099,7 +1099,7 @@ describe('Choices', () => { renderChoiceLimit: 4, }); - const renderedChoices = this.choices.choiceList.querySelectorAll('.choices__item'); + const renderedChoices = this.choices.choiceList.element.querySelectorAll('.choices__item'); expect(renderedChoices.length).toEqual(4); }); });