ssc-refer
Version:
React refer component for SSC 3.0
838 lines (756 loc) • 26.3 kB
JavaScript
'use strict';
import _extends from 'babel-runtime/helpers/extends';
import _JSON$stringify from 'babel-runtime/core-js/json/stringify';
import cx from 'classnames';
import { find, isEqual, noop, throttle, forEach, isArray } from 'lodash';
import onClickOutside from 'react-onclickoutside';
import React from 'react';
import PropTypes from 'prop-types';
import ClearButton from './ClearButton.react';
import Loader from './Loader.react';
import Overlay from './Overlay.react';
import TokenizerInput from './TokenizerInput.react';
import TypeaheadInput from './TypeaheadInput.react';
import TypeaheadMenu from './TypeaheadMenu.react';
import ReferList from './ReferList.react';
import ReferTable from './ReferTable.react';
import ReferTreeTable from './ReferTreeTable.react';
import addCustomOption from './utils/addCustomOption';
import defaultFilterBy from './utils/defaultFilterBy';
import getHintText from './utils/getHintText';
import getInputText from './utils/getInputText';
import getOptionLabel from './utils/getOptionLabel';
import getTruncatedOptions from './utils/getTruncatedOptions';
import warn from './utils/warn';
import request from 'superagent';
import { DOWN, ESC, RETURN, TAB, UP } from './utils/keyCode';
/**
* Refer
*/
var Refers = React.createClass({
displayName: 'Refers',
propTypes: {
/**
* Allows the creation of new selections on the fly. Note that any new items
* will be added to the list of selections, but not the list of original
* options unless handled as such by `Typeahead`'s parent.
*/
allowNew: PropTypes.bool,
/**
* Autofocus the input when the component initially mounts.
*/
autoFocus: PropTypes.bool,
/**
* Whether to render the menu inline or attach to `document.body`.
*/
bodyContainer: PropTypes.bool,
/**
* Whether or not filtering should be case-sensitive.
*/
caseSensitive: PropTypes.bool,
/**
* Displays a button to clear the input when there are selections.
*/
clearButton: PropTypes.bool,
/**
* Specify any pre-selected options. Use only if you want the component to
* be uncontrolled.
*/
defaultSelected: PropTypes.array,
/**
* Specify whether the menu should appear above the input.
*/
dropup: PropTypes.bool,
/**
* Either an array of fields in `option` to search, or a custom filtering
* callback.
*/
filterBy: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.string.isRequired), PropTypes.func]),
/**
* Whether the filter should ignore accents and other diacritical marks.
*/
ignoreDiacritics: PropTypes.bool,
/**
* Indicate whether an asynchromous data fetch is happening.
*/
/**
* Indicate whether an asynchromous data fetch is happening.
*/
isLoading: PropTypes.bool,
/**
* Specify the option key to use for display or a function returning the
* display string. By default, the selector will use the `label` key.
*/
labelKey: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
/**
* Maximum number of results to display by default. Mostly done for
* performance reasons so as not to render too many DOM nodes in the case of
* large data sets.
*/
maxResults: PropTypes.number,
/**
* Number of input characters that must be entered before showing results.
*/
minLength: PropTypes.number,
/**
* Whether or not multiple selections are allowed.
*/
multiple: PropTypes.bool,
/**
* Callback fired when the input is blurred. Receives an event.
*/
onBlur: PropTypes.func,
/**
* Callback fired whenever items are added or removed. Receives an array of
* the selected options.
*/
onChange: PropTypes.func,
/**
* Callback fired when the input is focused. Receives an event.
*/
onFocus: PropTypes.func,
/**
* Callback for handling changes to the user-input text.
*/
onInputChange: PropTypes.func,
/**
* Give user the ability to display additional results if the number of
* results exceeds `maxResults`.
*/
paginate: PropTypes.bool,
/**
* Callback for custom menu rendering.
*/
renderMenu: PropTypes.func,
/**
* The selected option(s) displayed in the input. Use this prop if you want
* to control the component via its parent.
*/
selected: PropTypes.array,
/**
* set refer data url ,for example `http://YOURHOST/queryRefJSON`
*/
referDataUrl: PropTypes.string.isRequired,
/**
* set refOptions ,for example `{"refCode":"dept","refType":"tree","rootName":"部门"}`
*/
referConditions: PropTypes.object.isRequired,
/**
* Is debug mode.
*/
requestHeader: PropTypes.object,
/**
* set refer type, for example: list, cascader, table, treetable, default type is list.
*/
referType: PropTypes.string.isRequired,
/**
* set custom columns for table display, for example `[{"field":"name", "label":"名称"},{"field":"code","label":"编码"},{"field":"addr","label":"地址"}]`
*/
tableColumns: PropTypes.array,
/**
* set refer whether to display the Disabled button
*/
showDisabledBtn: PropTypes.bool,
/**
* 显示停用按钮名称,默认值:'显示停用'
*/
showDisabledBtnText: PropTypes.string,
/**
* 隐藏停用按钮名称,默认值:'隐藏停用'
*/
showDisabledBtnText_Not: PropTypes.string,
/**
* set refer whether to display Disabled data
*/
showDisabled: PropTypes.bool,
/**
* Is debug mode.
*/
debugMode: PropTypes.bool,
/**
* search icon style
*/
searchIcon: PropTypes.string
},
getDefaultProps: function getDefaultProps() {
return {
allowNew: false,
autoFocus: false,
bodyContainer: false,
caseSensitive: false,
clearButton: false,
defaultSelected: [],
dropup: false,
filterBy: [],
ignoreDiacritics: true,
isLoading: false,
labelKey: 'label',
maxResults: 100,
minLength: 0,
multiple: false,
onBlur: noop,
onChange: noop,
onFocus: noop,
onInputChange: noop,
paginate: true,
selected: [],
referDataUrl: 'http://172.20.4.220/ficloud/refbase_ctr/queryRefJSON',
referConditions: {},
requestHeader: {},
referType: 'list',
debugMode: false,
showDisabledBtn: false,
showDisabledBtnText: '显示停用',
showDisabledBtnText_Not: '隐藏停用',
showDisabled: false,
searchIcon: 'glyphicon glyphicon-search refer-search-icon'
};
},
childContextTypes: {
activeIndex: PropTypes.number.isRequired,
onActiveItemChange: PropTypes.func.isRequired,
onInitialItemChange: PropTypes.func.isRequired,
onMenuItemClick: PropTypes.func.isRequired
},
getChildContext: function getChildContext() {
return {
activeIndex: this.state.activeIndex,
onActiveItemChange: this._handleActiveItemChange,
onInitialItemChange: this._handleInitialItemChange,
onMenuItemClick: this._handleAddOption
};
},
getInitialState: function getInitialState() {
var _props = this.props,
defaultSelected = _props.defaultSelected,
maxResults = _props.maxResults,
showDisabled = _props.showDisabled;
var selected = this.props.selected.slice();
if (defaultSelected && defaultSelected.length) {
selected = defaultSelected;
}
return {
activeIndex: -1,
activeItem: null,
initialItem: null,
selected: selected,
showMenu: false,
shownResults: maxResults,
text: '',
isAbove: true,
responseData: [],
styleStatus: { position: 'relative' },
showDisabled: showDisabled === undefined ? false : showDisabled
};
},
componentWillMount: function componentWillMount() {
var _props2 = this.props,
allowNew = _props2.allowNew,
labelKey = _props2.labelKey;
warn(!(typeof labelKey === 'function' && allowNew), '`labelKey` must be a string if creating new options is allowed.');
},
componentDidMount: function componentDidMount() {
this.props.autoFocus && this.focus();
},
componentWillReceiveProps: function componentWillReceiveProps(nextProps) {
var multiple = nextProps.multiple,
selected = nextProps.selected;
if (!isEqual(selected, this.props.selected)) {
// If new selections are passed in via props, treat the component as a
// controlled input.
this.setState({ selected: selected });
}
if (multiple !== this.props.multiple) {
this.setState({ text: '' });
}
},
render: function render() {
var _props3 = this.props,
allowNew = _props3.allowNew,
className = _props3.className,
dropup = _props3.dropup,
labelKey = _props3.labelKey,
paginate = _props3.paginate,
searchIcon = _props3.searchIcon;
var _state = this.state,
shownResults = _state.shownResults,
text = _state.text;
// First filter the results by the input string.
var results = this._getFilteredResults();
// This must come before we truncate.
var shouldPaginate = paginate && results.length > shownResults;
// Truncate if necessary.
if (shouldPaginate) {
results = getTruncatedOptions(results, shownResults);
}
// Add the custom option.
if (allowNew) {
results = addCustomOption(results, text, labelKey);
}
return React.createElement(
'div',
{
className: cx('bootstrap-typeahead', 'open', {
'dropup': dropup
}, className),
style: this.state.styleStatus },
this._renderInput(results),
this._renderAux(),
this._renderMenu(results, shouldPaginate)
);
},
_getFilteredResults: function _getFilteredResults() {
var _props4 = this.props,
caseSensitive = _props4.caseSensitive,
filterBy = _props4.filterBy,
ignoreDiacritics = _props4.ignoreDiacritics,
labelKey = _props4.labelKey,
minLength = _props4.minLength,
multiple = _props4.multiple;
var _state2 = this.state,
selected = _state2.selected,
text = _state2.text;
if (text.length < minLength) {
return [];
}
var callback = Array.isArray(filterBy) ? function (option) {
return defaultFilterBy(option, text, labelKey, multiple && !!find(selected, function (o) {
return isEqual(o, option);
}), { caseSensitive: caseSensitive, ignoreDiacritics: ignoreDiacritics, fields: filterBy });
} : function (option) {
return filterBy(option, text);
};
return this.state.responseData.filter(callback);
},
getFilteredSelected: function getFilteredSelected(responseData, selectedData) {
var result = void 0;
result = responseData.filter(function (item) {
var flag = false;
selectedData.map(function (obj) {
if (_JSON$stringify(obj) === _JSON$stringify(item)) flag = true;
});
return !flag;
});
return result;
},
_loadData: function _loadData() {
var _props5 = this.props,
referDataUrl = _props5.referDataUrl,
referConditions = _props5.referConditions,
requestHeader = _props5.requestHeader,
debugMode = _props5.debugMode,
selected = _props5.selected;
var _this = this;
referConditions['disableshow'] = _this.state.showDisabled;
request.post(referDataUrl).set(requestHeader).set('Content-Type', 'application/json').send(_JSON$stringify(referConditions)).end(function (err, res) {
if (err || !res.ok) {
if (debugMode) console.log('network error!' + err);
} else {
var data = JSON.parse(res.text);
if (data['success'] === undefined) {
if (debugMode) console.log('response data format is error,for example: no success key');
return false;
}
if (!data['success']) {
if (debugMode) console.log('response data success is false' + data['message']);
} else {
if (isArray(data.data) && data.data.length >= 0) {
_this.setState({ responseData: _this.getFilteredSelected(data.data, selected) });
} else {
if (debugMode) console.log('Message:' + 'Data format error, maybe no data !');
}
}
}
});
},
blur: function blur() {
this.refs.input.blur();
this._hideDropdown();
},
/**
* Public method to allow external clearing of the input. Clears both text
* and selection(s).
*/
clear: function clear() {
var _getInitialState = this.getInitialState(),
activeIndex = _getInitialState.activeIndex,
activeItem = _getInitialState.activeItem,
showMenu = _getInitialState.showMenu;
var selected = [];
var text = '';
this.setState({
activeIndex: activeIndex,
activeItem: activeItem,
selected: selected,
showMenu: showMenu,
text: text
});
this.props.onChange(selected);
this.props.onInputChange(text);
},
focus: function focus() {
this.refs.input.focus();
},
getData: function getData() {
return this.state.selected;
},
getInputTextValue: function getInputTextValue() {
var _props6 = this.props,
bsSize = _props6.bsSize,
disabled = _props6.disabled,
labelKey = _props6.labelKey,
minLength = _props6.minLength,
multiple = _props6.multiple,
name = _props6.name,
placeholder = _props6.placeholder,
renderToken = _props6.renderToken;
var _state3 = this.state,
activeIndex = _state3.activeIndex,
activeItem = _state3.activeItem,
initialItem = _state3.initialItem,
selected = _state3.selected,
text = _state3.text;
return getInputText({ activeItem: activeItem, labelKey: labelKey, multiple: multiple, selected: selected, text: text });
},
hideRefers: function hideRefers() {
this.setState({
styleStatus: { display: 'none' }
});
},
showRefers: function showRefers() {
this.setState({
styleStatus: { position: 'relative' }
});
},
_renderInput: function _renderInput(results) {
var _this2 = this;
var _props7 = this.props,
bsSize = _props7.bsSize,
disabled = _props7.disabled,
labelKey = _props7.labelKey,
minLength = _props7.minLength,
multiple = _props7.multiple,
name = _props7.name,
placeholder = _props7.placeholder,
renderToken = _props7.renderToken,
searchIcon = _props7.searchIcon,
showDisabledBtn = _props7.showDisabledBtn,
showDisabledBtnText = _props7.showDisabledBtnText,
showDisabledBtnText_Not = _props7.showDisabledBtnText_Not;
var _state4 = this.state,
activeIndex = _state4.activeIndex,
activeItem = _state4.activeItem,
initialItem = _state4.initialItem,
selected = _state4.selected,
text = _state4.text;
var Input = multiple ? TokenizerInput : TypeaheadInput;
var inputProps = { bsSize: bsSize, disabled: disabled, name: name, placeholder: placeholder, renderToken: renderToken };
return React.createElement(
'div',
{ className: 'input-group' },
React.createElement(Input, _extends({}, inputProps, {
activeIndex: activeIndex,
activeItem: activeItem,
hasAux: !!this._renderAux(),
hintText: getHintText({
activeItem: activeItem,
initialItem: initialItem,
labelKey: labelKey,
minLength: minLength,
selected: selected,
text: text
}),
initialItem: initialItem,
labelKey: labelKey,
onAdd: this._handleAddOption,
onBlur: this._handleBlur,
onChange: this._handleTextChange,
onFocus: this._handleFocus.bind(this, disabled),
onKeyDown: function onKeyDown(e) {
return _this2._handleKeydown(results, e);
},
onRemove: this._handleRemoveOption,
options: results,
ref: 'input',
selected: selected.slice(),
value: getInputText({ activeItem: activeItem, labelKey: labelKey, multiple: multiple, selected: selected, text: text })
})),
React.createElement(
'span',
{ className: "input-group-addon cursor-style", onClick: this._handleFocus.bind(this, disabled),
style: disabled ? { 'cursor': 'no-drop' } : {} },
React.createElement('span', { className: cx(searchIcon) })
),
showDisabledBtn === false ? null : React.createElement(
'span',
{ className: 'input-group-addon cursor-style', style: disabled ? { 'cursor': 'no-drop' } : {},
title: this.state.showDisabled ? showDisabledBtnText_Not || '隐藏停用' : showDisabledBtnText || '显示停用',
onClick: this._handleEnable.bind(this, disabled) },
React.createElement('span', { className: this.state.showDisabled ? 'icon-show-disabled red' : 'icon-show-disabled' })
)
);
},
_renderMenu: function _renderMenu(results, shouldPaginate) {
var _this3 = this;
var _props8 = this.props,
align = _props8.align,
bodyContainer = _props8.bodyContainer,
dropup = _props8.dropup,
emptyLabel = _props8.emptyLabel,
labelKey = _props8.labelKey,
maxHeight = _props8.maxHeight,
minLength = _props8.minLength,
newSelectionPrefix = _props8.newSelectionPrefix,
paginationText = _props8.paginationText,
renderMenu = _props8.renderMenu,
renderMenuItemChildren = _props8.renderMenuItemChildren,
referType = _props8.referType,
tableColumns = _props8.tableColumns;
var _state5 = this.state,
showMenu = _state5.showMenu,
text = _state5.text;
var menuProps = {
align: align,
dropup: dropup,
emptyLabel: emptyLabel,
labelKey: labelKey,
maxHeight: maxHeight,
newSelectionPrefix: newSelectionPrefix,
paginationText: paginationText,
onPaginate: this._handlePagination,
paginate: shouldPaginate,
text: text,
tableColumns: tableColumns
};
var list = renderMenu ? renderMenu(results, menuProps) : React.createElement(TypeaheadMenu, _extends({}, menuProps, {
options: results,
renderMenuItemChildren: renderMenuItemChildren
}));
var typeObj = noop;
switch (referType) {
case 'list':
typeObj = list;
break;
case 'cascader':
var cascader = renderMenu ? renderMenu(results, menuProps) : React.createElement(ReferList, _extends({}, menuProps, {
options: results,
renderMenuItemChildren: renderMenuItemChildren
}));
typeObj = cascader;
break;
case 'table':
var table = renderMenu ? renderMenu(results, menuProps) : React.createElement(ReferTable, _extends({}, menuProps, {
options: results,
onClickItem: this._handleAddOption
}));
typeObj = table;
break;
case 'treetable':
var treetable = renderMenu ? renderMenu(results, menuProps) : React.createElement(ReferTreeTable, _extends({}, menuProps, {
options: results,
onClickItem: this._handleAddOption
}));
typeObj = treetable;
break;
default:
typeObj = list;
}
return React.createElement(
Overlay,
{
container: bodyContainer ? document.body : this,
show: showMenu && text.length >= minLength,
target: function target() {
return _this3.refs.input;
} },
typeObj
);
},
_renderAux: function _renderAux() {
var _props9 = this.props,
bsSize = _props9.bsSize,
clearButton = _props9.clearButton,
disabled = _props9.disabled,
isLoading = _props9.isLoading;
if (isLoading) {
return React.createElement(Loader, { bsSize: bsSize });
}
if (clearButton && !disabled && this.state.selected.length) {
return React.createElement(ClearButton, {
bsSize: bsSize,
className: 'bootstrap-typeahead-clear-button',
onClick: this.clear
});
}
},
_handleActiveItemChange: function _handleActiveItemChange(activeItem) {
this.setState({ activeItem: activeItem });
},
_handleBlur: function _handleBlur(e) {
// Note: Don't hide the menu here, since that interferes with other actions
// like making a selection by clicking on a menu item.
if (this.props.onBlur) {
this.props.onBlur(e);
}
},
_handleFocus: function _handleFocus(disabled, e) {
if (disabled) {
return;
}
this.props.onFocus(e);
var multiple = this.props.multiple;
if (!multiple) {
// this.clear();
}
this._loadData();
this.setState({ showMenu: true });
},
_handleEnable: function _handleEnable(disabled, e) {
if (disabled) {
return;
}
this._handleBlur(e);
var showDisabled = this.state.showDisabled === undefined ? this.getInitialState() : this.state.showDisabled;
this.setState({ showDisabled: !showDisabled, showMenu: false });
},
_handleInitialItemChange: function _handleInitialItemChange(initialItem) {
var currentItem = this.state.initialItem;
if (!currentItem) {
this.setState({ initialItem: initialItem });
return;
}
var labelKey = this.props.labelKey;
// Don't update the initial item if it hasn't changed. For custom items,
// compare the `labelKey` values since a unique id is generated each time,
// causing the comparison to always return false otherwise.
if (isEqual(initialItem, currentItem) || initialItem.customOption && initialItem[labelKey] === currentItem[labelKey]) {
return;
}
this.setState({ initialItem: initialItem });
},
_handleTextChange: function _handleTextChange(text) {
var _getInitialState2 = this.getInitialState(),
activeIndex = _getInitialState2.activeIndex,
activeItem = _getInitialState2.activeItem;
this.setState({
activeIndex: activeIndex,
activeItem: activeItem,
showMenu: true,
text: text
});
this.props.onInputChange(text);
},
_handleChange: function _handleChange(text, e) {
this._handleTextChange(text);
this.props.onChange(e);
},
_handleKeydown: function _handleKeydown(options, e) {
var _state6 = this.state,
activeItem = _state6.activeItem,
showMenu = _state6.showMenu;
switch (e.keyCode) {
case UP:
case DOWN:
// Don't cycle through the options if the menu is hidden.
if (!showMenu) {
return;
}
var activeIndex = this.state.activeIndex;
// Prevents input cursor from going to the beginning when pressing up.
e.preventDefault();
// Increment or decrement index based on user keystroke.
activeIndex += e.keyCode === UP ? -1 : 1;
// If we've reached the end, go back to the beginning or vice-versa.
if (activeIndex === options.length) {
activeIndex = -1;
} else if (activeIndex === -2) {
activeIndex = options.length - 1;
}
var newState = { activeIndex: activeIndex };
if (activeIndex === -1) {
// Reset the active item if there is no active index.
newState.activeItem = null;
}
this.setState(newState);
break;
case ESC:
case TAB:
// Prevent closing dialogs.
e.keyCode === ESC && e.preventDefault();
this._hideDropdown();
break;
case RETURN:
// Prevent submitting forms.
e.preventDefault();
if (showMenu) {
activeItem && this._handleAddOption(activeItem);
}
break;
}
},
_handleAddOption: function _handleAddOption(selectedOption) {
var _props10 = this.props,
multiple = _props10.multiple,
labelKey = _props10.labelKey,
onChange = _props10.onChange,
onInputChange = _props10.onInputChange;
var selected = void 0;
var text = void 0;
if (multiple) {
// If multiple selections are allowed, add the new selection to the
// existing selections.
selected = this.state.selected.concat(selectedOption);
text = '';
} else {
// If only a single selection is allowed, replace the existing selection
// with the new one.
selected = [selectedOption];
text = getOptionLabel(selectedOption, labelKey);
}
this.setState({
initialItem: selectedOption,
selected: selected,
text: text
});
this._hideDropdown();
onChange(selected);
onInputChange(text);
},
_handlePagination: function _handlePagination(e) {
var shownResults = this.state.shownResults + this.props.maxResults;
// Keep the input focused when paginating.
this.focus();
this.setState({ shownResults: shownResults });
},
_handleRemoveOption: function _handleRemoveOption(removedOption) {
var selected = this.state.selected.slice();
selected = selected.filter(function (option) {
return !isEqual(option, removedOption);
});
// Make sure the input stays focused after the item is removed.
this.focus();
this.setState({ selected: selected });
this._hideDropdown();
this.props.onChange(selected);
},
/**
* From `onClickOutside` HOC.
*/
handleClickOutside: function handleClickOutside(e) {
this.state.showMenu && this._hideDropdown();
},
_hideDropdown: function _hideDropdown() {
var _getInitialState3 = this.getInitialState(),
activeIndex = _getInitialState3.activeIndex,
activeItem = _getInitialState3.activeItem,
showMenu = _getInitialState3.showMenu,
shownResults = _getInitialState3.shownResults;
this.setState({
activeIndex: activeIndex,
activeItem: activeItem,
showMenu: showMenu,
shownResults: shownResults
});
}
});
export default onClickOutside(Refers);