UNPKG

lin3s-front-foundation

Version:

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

568 lines (492 loc) 25.5 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _react = require('react'); var _react2 = _interopRequireDefault(_react); var _propTypes = require('prop-types'); var _propTypes2 = _interopRequireDefault(_propTypes); var _ArrowDown = require('./../../svg/ArrowDown'); var _ArrowDown2 = _interopRequireDefault(_ArrowDown); var _Loader = require('./../Loader/Loader'); var _Loader2 = _interopRequireDefault(_Loader); var _dispatchNativeEvent = require('../../../../dom/dispatchNativeEvent'); var _dispatchNativeEvent2 = _interopRequireDefault(_dispatchNativeEvent); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /* * 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> */ var isMobile = function isMobile(viewportWidth) { return viewportWidth < 1024; }; var optionShape = _propTypes2.default.shape({ label: _propTypes2.default.string, value: _propTypes2.default.string }); var FormSelect = function (_React$Component) { _inherits(FormSelect, _React$Component); function FormSelect(props) { _classCallCheck(this, FormSelect); var _this = _possibleConstructorReturn(this, (FormSelect.__proto__ || Object.getPrototypeOf(FormSelect)).call(this, props)); _this.optionNodesRefs = []; _this.state = { dataRendered: false, editingInput: false, focused: false, opened: false, touched: false, selectedOption: props.selectedOption || props.options[0], hoveredOption: null, mouseOverListenerEnabled: true }; // bre-bind method's context _this.onFocus = _this.onFocus.bind(_this); _this.onClick = _this.onClick.bind(_this); _this.onOutsideClick = _this.onOutsideClick.bind(_this); _this.onLabelClick = _this.onLabelClick.bind(_this); _this.onInputChange = _this.onInputChange.bind(_this); _this.onKeyDown = _this.onKeyDown.bind(_this); _this.onMouseDown = _this.onMouseDown.bind(_this); _this.onOptionMouseMove = _this.onOptionMouseMove.bind(_this); _this.onInputFocus = _this.onInputFocus.bind(_this); return _this; } _createClass(FormSelect, [{ key: 'onMouseDown', value: function onMouseDown() { this.mouseIsDown = true; } }, { key: 'onInputFocus', value: function onInputFocus(event) { event.persist(); event.stopPropagation(); } }, { key: 'onFocus', value: function onFocus() { this.setState({ focused: true }); if (this.tabHit) { return; } if (this.mouseIsDown) { return; } this.openSelect(); } }, { key: 'onInputChange', value: function onInputChange(event) { event.persist(); this.props.onInputChanged(event.target.value); } }, { key: 'onClick', value: function onClick(event) { this.mouseIsDown = false; if (!this.props.outsideClickToCloseEnabled) { return; } event.persist(); event.stopPropagation(); } }, { key: 'onLabelClick', value: function onLabelClick() { if (this.state.opened) { this.closeSelect(); } else { this.openSelect(); } } }, { key: 'onOutsideClick', value: function onOutsideClick() { this.closeSelect(); } }, { key: 'onKeyDown', value: function onKeyDown(event) { var keyCode = event.which; if (!(keyCode === 40 || keyCode === 38 || keyCode === 13 || event.which === 9)) { return; } if (keyCode === 9) { // tab if (this.state.opened) { this.tabHit = true; } this.closeSelect(); return; } if (!this.state.opened && this.state.focused && keyCode === 13) { this.openSelect(); return; } var hoveredOption = this.state.hoveredOption; var options = this.props.options; var hoveredOptionIndex = options.indexOf(hoveredOption); if (keyCode === 40) { // down hoveredOptionIndex = hoveredOptionIndex < 0 ? 0 : hoveredOptionIndex + 1 >= options.length ? options.length - 1 : hoveredOptionIndex + 1; this.setState({ hoveredOption: options[hoveredOptionIndex], mouseOverListenerEnabled: false }); this.optionNodesRefs[hoveredOptionIndex].scrollIntoView({ behaviour: 'smooth' }); } else if (keyCode === 38) { // up hoveredOptionIndex = hoveredOptionIndex < 0 ? 0 : hoveredOptionIndex - 1 < 0 ? 0 : hoveredOptionIndex - 1; this.setState({ hoveredOption: options[hoveredOptionIndex], mouseOverListenerEnabled: false }); this.optionNodesRefs[hoveredOptionIndex].scrollIntoView({ behaviour: 'smooth' }); } else if (keyCode === 13) { // enter this.onOptionSelected(hoveredOption); } } }, { key: 'onOptionSelected', value: function onOptionSelected(option) { if (!this.props.enabled) { return; } this.setState({ selectedOption: option }); this.props.onOptionSelected(option); this.closeSelect(); } }, { key: 'onOptionMouseMove', value: function onOptionMouseMove() { if (this.state.mouseOverListenerEnabled) { return; } this.setState({ mouseOverListenerEnabled: true }); } }, { key: 'onOptionMouseOver', value: function onOptionMouseOver(option) { if (!this.props.enabled) { return; } this.setState({ hoveredOption: option }); } }, { key: 'openSelect', value: function openSelect() { var _this2 = this; if (!this.props.enabled) { return; } var _state = this.state, hoveredOption = _state.hoveredOption, selectedOption = _state.selectedOption; var options = this.props.options; if (this.props.filterable) { this.filterInput.focus(); } this.setState(function () { return { opened: true, touched: true, editingInput: _this2.props.filterable, hoveredOption: selectedOption !== null ? selectedOption : hoveredOption !== null ? hoveredOption : options[0] }; }); this.setSelectFocusable(false); } }, { key: 'closeSelect', value: function closeSelect() { if (this.props.filterable) { this.filterInput.blur(); } this.tabHit = false; this.setState(function () { return { editingInput: false, focused: false, opened: false }; }); this.setSelectFocusable(true); } }, { key: 'setSelectFocusable', value: function setSelectFocusable(focusable) { var _this3 = this; setTimeout(function () { _this3.domNode.tabIndex = focusable ? 0 : -1; }, 100); } }, { key: 'getOptionsContainerHeight', value: function getOptionsContainerHeight() { var windowWidth = this.props.windowWidth; var totalHeight = this.optionNodesRefs.reduce(function (accumulatedHeight, optionNodeRef) { return accumulatedHeight + (optionNodeRef !== null ? optionNodeRef.getBoundingClientRect().height : 0); }, 0); if (isMobile(windowWidth) && totalHeight > FormSelect.OPTIONS_CONTAINER_MAX_HEIGHT_MOBILE) { return FormSelect.OPTIONS_CONTAINER_MAX_HEIGHT_MOBILE; } else if (!isMobile(windowWidth) && totalHeight > FormSelect.OPTIONS_CONTAINER_MAX_HEIGHT_DESKTOP) { return FormSelect.OPTIONS_CONTAINER_MAX_HEIGHT_DESKTOP; } return totalHeight; } }, { key: 'getOptionsContainerNeedsOverflow', value: function getOptionsContainerNeedsOverflow(optionsContainerHeight) { var windowWidth = this.props.windowWidth; return isMobile(windowWidth) && optionsContainerHeight >= FormSelect.OPTIONS_CONTAINER_MAX_HEIGHT_MOBILE || !isMobile(windowWidth) && optionsContainerHeight >= FormSelect.OPTIONS_CONTAINER_MAX_HEIGHT_DESKTOP; } }, { key: 'getDangerousHtml', value: function getDangerousHtml(rawHtml) { return { __html: rawHtml }; } }, { key: 'optionsAreEqual', value: function optionsAreEqual(props, nextProps) { return props !== undefined && props.options.length === nextProps.options.length && props.options.every(function (option) { return nextProps.options.find(function (nextPropsOption) { return nextPropsOption.label === option.label && nextPropsOption.value === option.value; }); }); } }, { key: 'componentWillReceiveProps', value: function componentWillReceiveProps(nextProps) { var optionsAreEqual = this.optionsAreEqual(this.props, nextProps), selectedOptionIsEqual = this.props.selectedOption === nextProps.selectedOption; this.setState(function (prevState) { var selectedOption = optionsAreEqual && nextProps.enabled ? selectedOptionIsEqual ? prevState.selectedOption : nextProps.selectedOption : nextProps.selectedOption || nextProps.options[0], hoveredOption = optionsAreEqual && nextProps.enabled ? prevState.selectedOption !== null ? prevState.selectedOption : prevState.hoveredOption : nextProps.options[0]; return { dataRendered: !optionsAreEqual, editingInput: nextProps.enabled && prevState.editingInput, opened: nextProps.enabled && prevState.opened, selectedOption: selectedOption, hoveredOption: hoveredOption }; }); } }, { key: 'componentDidMount', value: function componentDidMount() { if (this.props.outsideClickToCloseEnabled) { window.addEventListener('click', this.onOutsideClick); } /* eslint-disable react/no-did-mount-set-state */ this.setState({ dataRendered: true }); /* eslint-enable react/no-did-mount-set-state */ } }, { key: 'componentWillUnmount', value: function componentWillUnmount() { if (this.props.outsideClickToCloseEnabled) { window.removeEventListener('click', this.onOutsideClick); } } }, { key: 'componentDidUpdate', value: function componentDidUpdate(prevProps, prevState) { if (!this.optionsAreEqual(prevProps, this.props)) { /* eslint-disable react/no-did-update-set-state */ this.setState({ dataRendered: false }); /* eslint-enable react/no-did-update-set-state */ } if (prevState.selectedOption !== this.state.selectedOption) { (0, _dispatchNativeEvent2.default)(this.hiddenInput, 'input'); } } }, { key: 'shouldComponentUpdate', value: function shouldComponentUpdate(nextProps, nextState) { var _props = this.props, enabled = _props.enabled, filterValue = _props.filterValue, filterable = _props.filterable, id = _props.id, loading = _props.loading, onInputChanged = _props.onInputChanged, onOptionSelected = _props.onOptionSelected, options = _props.options, outsideClickToCloseEnabled = _props.outsideClickToCloseEnabled, placeholder = _props.placeholder, validationEnabled = _props.validationEnabled, validationPattern = _props.validationPattern, required = _props.required, selectedOption = _props.selectedOption; var _state2 = this.state, dataRendered = _state2.dataRendered, editingInput = _state2.editingInput, focused = _state2.focused, opened = _state2.opened, touched = _state2.touched, hoveredOption = _state2.hoveredOption, mouseOverListenerEnabled = _state2.mouseOverListenerEnabled; return enabled !== nextProps.enabled || filterValue !== nextProps.filterValue || filterable !== nextProps.filterable || id !== nextProps.id || placeholder !== nextProps.placeholder || loading !== nextProps.loading || onInputChanged !== nextProps.onInputChanged || onOptionSelected !== nextProps.onOptionSelected || outsideClickToCloseEnabled !== nextProps.outsideClickToCloseEnabled || validationEnabled !== nextProps.validationEnabled || validationPattern !== nextProps.validationPattern || required !== nextProps.required || selectedOption !== nextProps.selectedOption || !this.optionsAreEqual(this.props, nextProps) || dataRendered !== nextState.dataRendered || editingInput !== nextState.editingInput || focused !== nextState.focused || opened !== nextState.opened || touched !== nextState.touched || this.state.selectedOption !== nextState.selectedOption || hoveredOption !== nextState.hoveredOption || mouseOverListenerEnabled !== nextState.mouseOverListenerEnabled; } }, { key: 'render', value: function render() { var _this4 = this; /* eslint-disable react/no-danger */ var _state3 = this.state, dataRendered = _state3.dataRendered, editingInput = _state3.editingInput, opened = _state3.opened, focused = _state3.focused, selectedOption = _state3.selectedOption, hoveredOption = _state3.hoveredOption, mouseOverListenerEnabled = _state3.mouseOverListenerEnabled; var _props2 = this.props, enabled = _props2.enabled, filterable = _props2.filterable, filterValue = _props2.filterValue, id = _props2.id, placeholder = _props2.placeholder, loading = _props2.loading, options = _props2.options, required = _props2.required, validationEnabled = _props2.validationEnabled, validationPattern = _props2.validationPattern; var formSelectBaseClassName = 'form-select', formSelectClassName = formSelectBaseClassName + '\n ' + (opened && enabled ? ' form-select--opened' : '') + '\n ' + (loading ? ' form-select--loading' : '') + '\n ' + (!enabled ? ' form-select--disabled' : '') + '\n ' + (filterable && editingInput ? ' form-select--editing-input' : '') + '\n ' + (focused ? ' form-select--focused' : ''), selectLabel = selectedOption !== undefined && selectedOption !== null ? this.getDangerousHtml(selectedOption.label) : this.getDangerousHtml(options[0].label), optionsContainerHeight = opened ? this.getOptionsContainerHeight() : 0, needsOverflow = this.getOptionsContainerNeedsOverflow(optionsContainerHeight), optionsContainerClassName = 'form-select__options-container' + (!needsOverflow ? ' form-select__options-container--no-overflow' : ''); return _react2.default.createElement( 'div', { className: formSelectClassName, onClick: this.onClick, onFocus: this.onFocus, onKeyDown: this.onKeyDown, onMouseDown: this.onMouseDown, ref: function ref(domNode) { _this4.domNode = domNode; }, tabIndex: '0' }, _react2.default.createElement( 'div', { className: 'form-select__loader' }, _react2.default.createElement(_Loader2.default, null) ), _react2.default.createElement( 'span', { className: 'form-select__dropdown-icon' }, _react2.default.createElement(_ArrowDown2.default, null) ), filterable && _react2.default.createElement( 'div', { className: 'form-select__input-container' }, _react2.default.createElement('input', { className: 'form-input form-select__input', onChange: this.onInputChange, onFocus: this.onInputFocus, placeholder: placeholder, ref: function ref(input) { _this4.filterInput = input; }, tabIndex: '-1', type: 'text', value: filterValue }) ), _react2.default.createElement('div', { className: 'form-select__label', dangerouslySetInnerHTML: selectLabel, onClick: this.onLabelClick }), _react2.default.createElement( 'div', { className: optionsContainerClassName, style: { height: optionsContainerHeight } }, _react2.default.createElement( 'div', { className: 'form-select__options' }, options && options.map(function (option, index) { var onOptionMouseOver = mouseOverListenerEnabled ? _this4.onOptionMouseOver.bind(_this4, option) : null, formOptionClassName = 'form-select__option\n ' + (selectedOption && option.value === selectedOption.value ? ' form-select__option--active' : '') + '\n ' + (hoveredOption && option.value === hoveredOption.value ? ' form-select__option--hover' : ''), labelHtml = _this4.getDangerousHtml(option.label); return _react2.default.createElement('div', { className: formOptionClassName, dangerouslySetInnerHTML: labelHtml, key: 'form-select-view-' + index, onClick: _this4.onOptionSelected.bind(_this4, option), onMouseMove: _this4.onOptionMouseMove, onMouseOver: onOptionMouseOver, ref: function ref(optionNode) { _this4.optionNodesRefs[index] = optionNode; } }); }) ) ), _react2.default.createElement('input', { className: 'form-select__select', id: id, name: id, 'data-validate': validationEnabled ? true : null, 'data-validate-pattern': validationEnabled ? validationPattern : null, 'data-validation-state-reference-selector': validationEnabled ? '.' + formSelectBaseClassName : null, ref: function ref(_ref) { _this4.hiddenInput = _ref; }, required: required, tabIndex: '-1', type: 'hidden', value: selectedOption !== undefined && selectedOption !== null ? selectedOption.value : '' }) ); /* eslint-enable react/no-danger */ } }]); return FormSelect; }(_react2.default.Component); FormSelect.OPTIONS_CONTAINER_MAX_HEIGHT_MOBILE = 260; FormSelect.OPTIONS_CONTAINER_MAX_HEIGHT_DESKTOP = 275; FormSelect.propTypes = { enabled: _propTypes2.default.bool, filterValue: _propTypes2.default.string, filterable: _propTypes2.default.bool, id: _propTypes2.default.string.isRequired, loading: _propTypes2.default.bool, onInputChanged: _propTypes2.default.func, onOptionSelected: _propTypes2.default.func.isRequired, options: _propTypes2.default.arrayOf(optionShape), outsideClickToCloseEnabled: _propTypes2.default.bool, placeholder: _propTypes2.default.string, selectedOption: optionShape, validationEnabled: _propTypes2.default.bool, validationPattern: _propTypes2.default.string, required: _propTypes2.default.bool }; FormSelect.defaultProps = { enabled: true, filterValue: '', filterable: false, loading: false, onInputChanged: function onInputChanged() {}, outsideClickToCloseEnabled: true, validationEnabled: false, validationPattern: '', required: false }; exports.default = FormSelect;