UNPKG

lin3s-front-foundation

Version:

Library that provides a sort of commonly used front-end components in LIN3S projects

520 lines (411 loc) 16.5 kB
/* * This file is part of the Front Foundation package. * * Copyright (c) 2017-present LIN3S <info@lin3s.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @author Mikel Tuesta <mikeltuesta@gmail.com> */ import debounce from 'lodash.debounce'; import {onWindowResized, OneTimeEventPublisher, LifeTimeEventPublisher} from 'lin3s-event-bus'; import {getHtmlLang} from './../../../../dom'; import FormSelectInitializedEvent from './../../../../event-bus/FormSelect/FormSelectInitializedEvent'; import FormSelectOptionSelectedEvent from './../../../../event-bus/FormSelect/FormSelectOptionSelectedEvent'; import FormSelectStateChangedEvent from './../../../../event-bus/FormSelect/FormSelectStateChangedEvent'; import addSelectorFilteredEventListener from '../../../../dom/addSelectorFilteredEventListener'; import getDomNodeIndex from '../../../../dom/getDomNodeIndex'; import removeDomNodes from '../../../../dom/removeDomNodes'; import dispatchNativeEvent from '../../../../dom/dispatchNativeEvent'; const renderSelectOption = (optionValue, optionLabel) => { const option = document.createElement('option'); option.value = optionValue; option.textContent = optionLabel; return option; }, renderSelectOptionView = (optionValue, optionLabel) => { const optionView = document.createElement('div'); optionView.classList.add('form-select__option'); optionView.dataset.label = optionLabel; optionView.dataset.value = optionValue; optionView.textContent = optionLabel; return optionView; }; const isMobile = (viewportWidth, mobileBreakpoint = 1024) => viewportWidth < mobileBreakpoint; class FormSelect { static STATE = { OPENED: 'OPENED', CLOSED: 'CLOSED' }; constructor(domNode) { this.domNode = domNode; this.opened = this.domNode.classList.contains('form-select--opened'); this.focused = false; this.enabled = true; this.maxHeightDesktop = parseInt(this.domNode.dataset.maxHeightDesktop, 10); this.maxHeightMobile = parseInt(this.domNode.dataset.maxHeightMobile, 10); this.isFilterable = parseInt(this.domNode.dataset.filterable, 10) === 1; this.filterBy = this.domNode.dataset.filterOrderBy; this.mobileBreakpoint = parseInt(this.domNode.dataset.mobileBreakpoint, 10); this.outsideClickToCloseEnabled = parseInt(this.domNode.dataset.outsideClickToCloseEnabled, 10) === 1; this.mouseOverListenerEnabled = true; this.keyboardControlledSelectedOptionIndex = 0; this.lastFilterValue = ''; this.openedByFocus = false; if (this.isFilterable) { this.filterInput = this.domNode.querySelector('.form-select__input'); } this.select = this.domNode.querySelector('.form-select__select'); this.selectOptionsContainer = this.domNode.querySelector('.form-select__options-container'); this.selectOptions = this.domNode.querySelector('.form-select__options'); this.selectLabel = this.domNode.querySelector('.form-select__label'); this.debouncedFilter = debounce(() => this.filter(), 500); this.parseSelectOptions(); this.bindListeners(); this.setSelectOpened(this.opened); this.setInitiallySelectedOption(); this.publishFormSelectInitializedEvent(); } parseSelectOptions() { const selectOptions = this.select.querySelectorAll('option'); this.options = Array.from(selectOptions).map(selectOption => { return { value: selectOption.value.trim(), label: selectOption.textContent.trim() }; }); } getOptionsContainerNeedsOverflow(optionsContainerHeight) { return isMobile(this.windowWidth, this.mobileBreakpoint) && optionsContainerHeight >= this.maxHeightMobile || !isMobile(this.windowWidth, this.mobileBreakpoint) && optionsContainerHeight >= this.maxHeightDesktop; } setOptionsContainerHeight(height) { // Prevent overflow/scroll animation this.selectOptionsContainer.classList.toggle('form-select__options-container--no-overflow', !this.getOptionsContainerNeedsOverflow(height)); this.selectOptionsContainer.style.height = `${height}px`; } getOptionsContainerHeight() { const totalHeight = Array.from(this.domNode.querySelectorAll('.form-select__option')) .reduce((accumulatedHeight, option) => accumulatedHeight + option.offsetHeight, 0); if (isMobile(this.windowWidth, this.mobileBreakpoint) && totalHeight >= this.maxHeightMobile) { return this.maxHeightMobile; } else if (!isMobile(this.windowWidth, this.mobileBreakpoint) && totalHeight >= this.maxHeightDesktop) { return this.maxHeightDesktop; } return totalHeight; } bindListeners() { if (this.isFilterable) { this.filterInput.addEventListener('input', () => this.debouncedFilter()); this.filterInput.addEventListener('click', event => { if (this.opened) { event.stopPropagation(); } }); } if (this.outsideClickToCloseEnabled) { window.addEventListener('click', this.close.bind(this)); } this.domNode.addEventListener('focus', () => { this.setSelectFocused(true); if (this.tabHit) { return; } if (!this.isFilterable && !this.opened) { this.openedByFocus = true; } this.open(); }); addSelectorFilteredEventListener(this.domNode, 'click', '.form-select__option', this.onClick.bind(this)); this.domNode.addEventListener('click', event => { if (this.outsideClickToCloseEnabled) { event.stopPropagation(); } if (event.target === this.domNode || this.openedByFocus) { return; } this.setSelectOpened(!this.opened); }); addSelectorFilteredEventListener(this.domNode, 'mouseover', '.form-select__option', this.onMouseOver.bind(this)); addSelectorFilteredEventListener(this.domNode, 'mousemove', '.form-select__option', this.onMouseMove.bind(this)); this.domNode.addEventListener('keydown', this.onKeyDown.bind(this)); onWindowResized(this.onResize.bind(this)); } onClick(event) { this.setSelectedOption(event.target); } onMouseOver(event) { if (!this.mouseOverListenerEnabled) { return; } this.setKeyboardControlledSelectedOptionView( this.domNode.querySelectorAll('.form-select__option'), event.target ); this.keyboardControlledSelectedOptionIndex = getDomNodeIndex(event.target); } onMouseMove(event) { this.mouseOverListenerEnabled = true; } onKeyDown(event) { if (!(event.which === 40 || event.which === 38 || event.which === 13 || event.which === 9)) { return; } if (event.which === 9) { // tab if (this.opened) { this.tabHit = true; } this.close(); return; } event.preventDefault(); this.mouseOverListenerEnabled = false; const selectOptionViews = Array.from(this.domNode.querySelectorAll('.form-select__option')); if (event.which === 13) { // enter if (!this.opened && this.focused) { this.open(); } else { this.setSelectedOption(selectOptionViews[this.keyboardControlledSelectedOptionIndex]); this.close(); } } else if (event.which === 40) { // down this.keyboardControlledSelectedOptionIndex = this.keyboardControlledSelectedOptionIndex < selectOptionViews.length - 1 ? this.keyboardControlledSelectedOptionIndex + 1 : selectOptionViews.length - 1; this.setKeyboardControlledSelectedOptionView(selectOptionViews, selectOptionViews[this.keyboardControlledSelectedOptionIndex], true); } else if (event.which === 38) { // up this.keyboardControlledSelectedOptionIndex = this.keyboardControlledSelectedOptionIndex > 0 ? this.keyboardControlledSelectedOptionIndex - 1 : 0; this.setKeyboardControlledSelectedOptionView(selectOptionViews, selectOptionViews[this.keyboardControlledSelectedOptionIndex], true); } } onResize(windowResizedEvent) { this.windowWidth = windowResizedEvent.windowWidth || window.innerWidth; if (this.opened) { this.open(); } } setInitiallySelectedOption() { const selectedOption = this.select.querySelector('option[selected="selected"]'); if (selectedOption === null) { return; } this.onOptionSelected(selectedOption.textContent, selectedOption.value); } setSelectOpened(opened) { if (opened) { this.open(); } else { this.close(); } this.publishFormSelectStateChangedEvent(opened ? FormSelect.STATE.OPENED : FormSelect.STATE.CLOSED); } setSelectFocused(focused) { this.focused = focused; this.domNode.classList.toggle('form-select--focused', focused); } setSelectFocusable(focusable) { setTimeout(() => { this.domNode.tabIndex = focusable ? 0 : -1; }, 100); } open() { if (!this.enabled) { return; } this.domNode.classList.add('form-select--opened'); this.domNode.classList.toggle('form-select--editing-input', this.isFilterable); this.setOptionsContainerHeight(this.getOptionsContainerHeight()); this.opened = true; this.setSelectFocused(false); if (this.isFilterable) { setTimeout(() => { if (this.opened) { this.filterInput.focus(); } }, 100); } this.setSelectFocusable(false); } close() { if (!this.enabled) { return; } this.domNode.classList.remove('form-select--opened'); this.domNode.classList.remove('form-select--editing-input'); this.setOptionsContainerHeight(0); this.opened = false; this.setSelectFocused(false); this.tabHit = false; if (this.isFilterable) { this.filterInput.blur(); } this.setSelectFocusable(true); this.openedByFocus = false; } onOptionSelected(optionLabel, optionValue, publishEvent = true) { if (!this.enabled) { return; } // Explicitly cast to String const label = String(optionLabel), value = String(optionValue); if (this.selectedOptionLabel === optionLabel || this.selectedOptionValue === optionValue) { return; } this.setSelectValue(value); this.setSelectLabelValue(label); this.setSelectActiveOption(value); this.selectedOptionLabel = optionLabel; this.selectedOptionValue = optionValue; if ((this.selectedOptionLabel !== undefined || this.selectedOptionValue !== undefined) && publishEvent) { this.publishFormSelectOptionSelectedEvent(value); } this.openedByFocus = false; } setSelectValue(value) { this.select.value = value; dispatchNativeEvent(this.select, 'input'); } setSelectLabelValue(value) { this.selectLabel.textContent = value; } setSelectActiveOption(optionValue) { Array.from(this.domNode.querySelectorAll('.form-select__option')).forEach(selectOption => selectOption.classList.toggle('form-select__option--active', String(selectOption.dataset.value) === String(optionValue) )); } setSelectedOptionByOptionValue(optionValue) { const option = this.domNode.querySelector(`.form-select__option[data-value="${optionValue}"]`), label = option.getAttribute('data-label'); this.onOptionSelected(label, optionValue, false); } update({options, publishEvent = true} = {}) { this.close(); this.destroy(); if (options.length === 0) { return; } this.build(options); this.setSelectedOptionDefault(publishEvent); } destroy() { removeDomNodes( this.domNode.querySelectorAll('.form-select__option:not(.form-select__option--select-no-selection), option:not(.form-select__select-no-selection)') // eslint-disable-line max-len ); this.filterInput.value = ''; } build(options) { this.options = options; this.options.forEach(option => { this.select.appendChild(renderSelectOption(option.value, option.label)); this.selectOptions.appendChild(renderSelectOptionView(option.value, option.label)); }); this.setOptionsContainerHeight(this.getOptionsContainerHeight()); } filter() { const noSelectionValue = this.domNode.querySelector('.form-select__select-no-selection').value, filterValue = this.filterInput.value, filteredOptions = this.options.filter(option => option.label.toLowerCase().includes(filterValue.toLowerCase()) || filterValue === '' || option.value === noSelectionValue ); if (filterValue === this.lastFilterValue) { return; } this.lastFilterValue = filterValue; // Remove unnecessary ones this.options.forEach(option => { if (filteredOptions.indexOf(option) === -1) { const optionView = this.selectOptions.querySelectorAll(`[data-value="${option.value}"]`); removeDomNodes(optionView); } }); // Add necessary ones filteredOptions.forEach(filteredOption => { const selectOption = this.domNode.querySelector(`.form-select__option[data-value="${filteredOption.value}"]`); if (selectOption === null) { // Find place to inject const currentSelectOptionViews = Array.from(this.domNode.querySelectorAll('.form-select__option + .form-select__option')), // eslint-disable-line max-len newSelectOption = renderSelectOption(filteredOption.value, filteredOption.label), newSelectOptionView = renderSelectOptionView(filteredOption.value, filteredOption.label); let found = false, index = 0; while (index < currentSelectOptionViews.length && !found) { const currentSelectOption = currentSelectOptionViews[index]; const isInteger = (!isNaN(parseInt(currentSelectOption.getAttribute(`data-${this.filterBy}`), 10)) && !isNaN(parseInt(filteredOption[`${this.filterBy}`], 10))); const currentOptionIsBiggerThanFilteredOne = isInteger ? parseInt(currentSelectOption.getAttribute(`data-${this.filterBy}`), 10) > parseInt(filteredOption[`${this.filterBy}`], 10) // eslint-disable-line max-len : String(currentSelectOption.getAttribute(`data-${this.filterBy}`)).trim().localeCompare(filteredOption[`${this.filterBy}`], getHtmlLang(), {sensitivity: 'base'}) > 0; // eslint-disable-line max-len if (currentOptionIsBiggerThanFilteredOne) { found = true; } index++; } if (found) { const currentOption = Array.from(this.select.querySelectorAll('option'))[index], currentOptionView = Array.from(this.domNode.querySelectorAll('.form-select__option'))[index]; this.select.insertBefore(newSelectOption, currentOption); this.selectOptions.insertBefore(newSelectOptionView, currentOptionView); } else { this.select.appendChild(newSelectOption); this.selectOptions.appendChild(newSelectOptionView); } } }); this.setOptionsContainerHeight(this.getOptionsContainerHeight()); // select none this.setSelectedOptionDefault(false); } setSelectedOptionDefault(publishEvent = true) { const noSelection = this.domNode.querySelector('.form-select__select-no-selection'); this.onOptionSelected(noSelection.textContent.trim(), noSelection.value.trim(), publishEvent); } setSelectedOption(option) { this.onOptionSelected(option.dataset.label, option.dataset.value); } setKeyboardControlledSelectedOptionView(selectOptionViews, optionView, animateScroll = false) { Array.from(selectOptionViews).forEach(selectOptionView => selectOptionView.classList.remove('form-select__option--hover')); if (optionView === undefined) { return; } optionView.classList.add('form-select__option--hover'); // Scroll to option if (!animateScroll) { return; } optionView.scrollIntoView({behaviour: 'smooth'}); } enable() { this.enabled = true; } disable() { this.enabled = false; } publishFormSelectInitializedEvent() { OneTimeEventPublisher.publish(new FormSelectInitializedEvent(this)); } publishFormSelectOptionSelectedEvent(value) { LifeTimeEventPublisher.publish(new FormSelectOptionSelectedEvent(this, value)); } publishFormSelectStateChangedEvent(state) { LifeTimeEventPublisher.publish(new FormSelectStateChangedEvent(this, state)); } } export default FormSelect;