/** * MUI React select module * @module react/select */ 'use strict'; import React from 'react'; import * as formlib from '../js/lib/forms'; import * as jqLite from '../js/lib/jqLite'; import * as util from '../js/lib/util'; import { controlledMessage } from './_helpers'; /** * Select constructor * @class */ class Select extends React.Component { constructor(props) { super(props); // warn if value defined but onChange is not if (props.readOnly === false && props.value !== undefined && props.onChange === null) { util.raiseError(controlledMessage, true); } this.state.value = props.value; // bind callback function let cb = util.callback; this.onInnerChangeCB = cb(this, 'onInnerChange'); this.onInnerMouseDownCB = cb(this, 'onInnerMouseDown'); this.onOuterClickCB = cb(this, 'onOuterClick'); this.onOuterKeyDownCB = cb(this, 'onOuterKeyDown'); this.hideMenuCB = cb(this, 'hideMenu'); this.onMenuChangeCB = cb(this, 'onMenuChange'); } state = { showMenu: false }; static defaultProps = { className: '', name: '', placeholder: null, readOnly: false, useDefault: (typeof document !== 'undefined' && 'ontouchstart' in document.documentElement) ? true : false, onChange: null, onClick: null, onKeyDown: null }; componentDidMount() { // disable MUI CSS/JS this.controlEl._muiSelect = true; } componentWillReceiveProps(nextProps) { this.setState({ value: nextProps.value }); } componentWillUnmount() { // ensure that doc event listners have been removed jqLite.off(window, 'resize', this.hideMenuCB); jqLite.off(document, 'click', this.hideMenuCB); } onInnerChange(ev) { // update state this.setState({value: ev.target.value}); } onInnerMouseDown(ev) { // only left clicks & check flag if (ev.button !== 0 || this.props.useDefault) return; // prevent built-in menu from opening ev.preventDefault(); } onOuterClick(ev) { // only left clicks, return if and dispatch 'change' event this.controlEl.selectedIndex = index; util.dispatchEvent(this.controlEl, 'change'); } render() { let value = this.state.value, valueArgs = {}, menuElem, placeholderElem, selectCls; if (this.state.showMenu) { menuElem = ( ); } // set tab index so user can focus wrapper element let tabIndexWrapper = '-1', tabIndexInner = '0'; if (this.props.useDefault === false) { tabIndexWrapper = '0'; tabIndexInner = '-1'; } const { children, className, style, label, defaultValue, readOnly, disabled, useDefault, name, placeholder, ...reactProps } = this.props; // build value arguments if (this.props.value !== undefined) valueArgs.value = value; // controlled if (defaultValue !== undefined) valueArgs.defaultValue = defaultValue; // handle placeholder if (placeholder) { placeholderElem = ( ); // apply class if value is empty if (value === '' || (value === undefined && !defaultValue)) { selectCls = 'mui--text-placeholder'; } } return (
{ this.wrapperElRef = el }} tabIndex={tabIndexWrapper} style={style} className={'mui-select ' + className} onClick={this.onOuterClickCB} onKeyDown={this.onOuterKeyDownCB} > {menuElem}
); } } /** * Menu constructor * @class */ class Menu extends React.Component { constructor(props) { super(props); this.onKeyDownCB = util.callback(this, 'onKeyDown'); this.onKeyPressCB = util.callback(this, 'onKeyPress'); this.q = ''; this.qTimeout = null; this.availOptionEls = []; // extract selectable options let optionEls = props.optionEls, el, i; for (i=0; i < optionEls.length; i++) { el = optionEls[i]; if (!el.disabled && !el.hidden) this.availOptionEls.push(el); } } state = { origIndex: null, currentIndex: 0 }; static defaultProps = { optionEls: [], wrapperEl: null, onChange: null, onClose: null }; componentWillMount() { let optionEls = this.availOptionEls, m = optionEls.length, selectedPos = null, i; // get current selected position for (i = m - 1; i > -1; i--) if (optionEls[i].selected) selectedPos = i; if (selectedPos !== null) { this.setState({ origIndex: selectedPos, currentIndex: selectedPos }); } } componentDidMount() { // prevent scrolling util.enableScrollLock(); let menuEl = this.wrapperElRef; // set position let props = formlib.getMenuPositionalCSS( this.props.wrapperEl, menuEl, this.state.currentIndex ); jqLite.css(menuEl, props); jqLite.scrollTop(menuEl, props.scrollTop); // attach keydown handler jqLite.on(document, 'keydown', this.onKeyDownCB); jqLite.on(document, 'keypress', this.onKeyPressCB); } componentWillUnmount() { // remove scroll lock util.disableScrollLock(true); // remove keydown handler jqLite.off(document, 'keydown', this.onKeyDownCB); jqLite.off(document, 'keypress', this.onKeyPressCB); } onClick(pos, ev) { // don't allow events to bubble //ev.stopPropagation(); ev.preventDefault(); if (pos !== null) this.selectAndDestroy(pos); } onKeyDown(ev) { let keyCode = ev.keyCode; // tab if (keyCode === 9) return this.destroy(); // escape | up | down | enter if (keyCode === 27 || keyCode === 40 || keyCode === 38 || keyCode === 13) { ev.preventDefault(); } if (keyCode === 27) this.destroy(); else if (keyCode === 40) this.increment(); else if (keyCode === 38) this.decrement(); else if (keyCode === 13) this.selectAndDestroy(); } onKeyPress(ev) { // handle query timer let self = this; clearTimeout(this.qTimeout); this.q += ev.key; this.qTimeout = setTimeout(function () { self.q = ''; }, 300); // select first match alphabetically let prefixRegex = new RegExp('^' + this.q, 'i'), optionEls = this.availOptionEls, m = optionEls.length, i; for (i = 0; i < m; i++) { // select item if code matches if (prefixRegex.test(optionEls[i].innerText)) { this.setState({ currentIndex: i }); break; } } } increment() { if (this.state.currentIndex === this.availOptionEls.length - 1) return; this.setState({ currentIndex: this.state.currentIndex + 1 }); } decrement() { if (this.state.currentIndex === 0) return; this.setState({ currentIndex: this.state.currentIndex - 1 }); } selectAndDestroy(pos) { pos = (pos === undefined) ? this.state.currentIndex : pos; // handle onChange if (pos !== this.state.origIndex) { this.props.onChange(this.availOptionEls[pos].index); } // close menu this.destroy(); } destroy() { this.props.onClose(); } componentDidUpdate(prevProps, prevState) { // scroll menu (if necessary) if (this.state.currentIndex != prevState.currentIndex) { var menuEl = this.wrapperElRef, itemEl = menuEl.children[this.state.currentIndex], itemRect = itemEl.getBoundingClientRect(); if (itemRect.top < 0) { // menu item is hidden above visible window menuEl.scrollTop = menuEl.scrollTop + itemRect.top - 5; } else if (itemRect.top > window.innerHeight) { // menu item is hidden below visible window menuEl.scrollTop = menuEl.scrollTop + (itemRect.top + itemRect.height - window.innerHeight) + 5; } } } render() { let menuItems = [], optionEls = this.props.optionEls, m = optionEls.length, pos = 0, optionEl, cls, val, i; // define menu items for (i = 0; i < m; i++) { optionEl = optionEls[i]; // handle hidden if (optionEl.hidden) continue; // handle disabled if (optionEl.disabled) { cls = 'mui--is-disabled '; val = null; } else { cls = (pos === this.state.currentIndex) ? 'mui--is-selected ' : ''; val = pos; pos += 1; } // add custom css class from