diff --git a/.gitignore b/.gitignore index b0e0e191..96eb02f9 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,5 @@ node_modules *.css build lib -es yarn.lock package-lock.json diff --git a/es/Cascader.js b/es/Cascader.js new file mode 100644 index 00000000..e3ad5cf5 --- /dev/null +++ b/es/Cascader.js @@ -0,0 +1,490 @@ +import _extends from 'babel-runtime/helpers/extends'; +import _objectWithoutProperties from 'babel-runtime/helpers/objectWithoutProperties'; +import _toConsumableArray from 'babel-runtime/helpers/toConsumableArray'; +import _classCallCheck from 'babel-runtime/helpers/classCallCheck'; +import _createClass from 'babel-runtime/helpers/createClass'; +import _possibleConstructorReturn from 'babel-runtime/helpers/possibleConstructorReturn'; +import _inherits from 'babel-runtime/helpers/inherits'; +import React, { Component, cloneElement } from 'react'; +import PropTypes from 'prop-types'; +import Trigger from 'rc-trigger'; +import warning from 'warning'; +import KeyCode from 'rc-util/es/KeyCode'; +import arrayTreeFilter from 'array-tree-filter'; +import shallowEqualArrays from 'shallow-equal/arrays'; +import { polyfill } from 'react-lifecycles-compat'; +import Menus from './Menus'; + +var BUILT_IN_PLACEMENTS = { + bottomLeft: { + points: ['tl', 'bl'], + offset: [0, 4], + overflow: { + adjustX: 1, + adjustY: 1, + }, + }, + topLeft: { + points: ['bl', 'tl'], + offset: [0, -4], + overflow: { + adjustX: 1, + adjustY: 1, + }, + }, + bottomRight: { + points: ['tr', 'br'], + offset: [0, 4], + overflow: { + adjustX: 1, + adjustY: 1, + }, + }, + topRight: { + points: ['br', 'tr'], + offset: [0, -4], + overflow: { + adjustX: 1, + adjustY: 1, + }, + }, +}; + +var Cascader = (function(_Component) { + _inherits(Cascader, _Component); + + function Cascader(props) { + _classCallCheck(this, Cascader); + + var _this = _possibleConstructorReturn( + this, + (Cascader.__proto__ || Object.getPrototypeOf(Cascader)).call(this, props), + ); + + _this.setPopupVisible = function(popupVisible) { + if (!('popupVisible' in _this.props)) { + _this.setState({ popupVisible: popupVisible }); + } + // sync activeValue with value when panel open + if (popupVisible && !_this.state.popupVisible) { + _this.setState({ + activeValue: _this.state.value, + }); + } + _this.props.onPopupVisibleChange(popupVisible); + }; + + _this.handleChange = function(options, setProps, e) { + if (e.type !== 'keydown' || e.keyCode === KeyCode.ENTER) { + _this.props.onChange( + options.map(function(o) { + return o[_this.getFieldName('value')]; + }), + options, + ); + _this.setPopupVisible(setProps.visible); + } + }; + + _this.handlePopupVisibleChange = function(popupVisible) { + _this.setPopupVisible(popupVisible); + }; + + _this.handleMenuSelect = function(targetOption, menuIndex, e) { + // Keep focused state for keyboard support + var triggerNode = _this.trigger.getRootDomNode(); + if (triggerNode && triggerNode.focus) { + triggerNode.focus(); + } + var _this$props = _this.props, + changeOnSelect = _this$props.changeOnSelect, + loadData = _this$props.loadData, + expandTrigger = _this$props.expandTrigger; + + if (!targetOption || targetOption.disabled) { + return; + } + var activeValue = _this.state.activeValue; + + activeValue = activeValue.slice(0, menuIndex + 1); + activeValue[menuIndex] = targetOption[_this.getFieldName('value')]; + var activeOptions = _this.getActiveOptions(activeValue); + if ( + targetOption.isLeaf === false && + !targetOption[_this.getFieldName('children')] && + loadData + ) { + if (changeOnSelect) { + _this.handleChange(activeOptions, { visible: true }, e); + } + _this.setState({ activeValue: activeValue }); + loadData(activeOptions); + return; + } + var newState = {}; + if ( + !targetOption[_this.getFieldName('children')] || + !targetOption[_this.getFieldName('children')].length + ) { + _this.handleChange(activeOptions, { visible: false }, e); + // set value to activeValue when select leaf option + newState.value = activeValue; + // add e.type judgement to prevent `onChange` being triggered by mouseEnter + } else if (changeOnSelect && (e.type === 'click' || e.type === 'keydown')) { + if (expandTrigger === 'hover') { + _this.handleChange(activeOptions, { visible: false }, e); + } else { + _this.handleChange(activeOptions, { visible: true }, e); + } + // set value to activeValue on every select + newState.value = activeValue; + } + newState.activeValue = activeValue; + // not change the value by keyboard + if ('value' in _this.props || (e.type === 'keydown' && e.keyCode !== KeyCode.ENTER)) { + delete newState.value; + } + _this.setState(newState); + }; + + _this.handleItemDoubleClick = function() { + var changeOnSelect = _this.props.changeOnSelect; + + if (changeOnSelect) { + _this.setPopupVisible(false); + } + }; + + _this.handleKeyDown = function(e) { + var children = _this.props.children; + // https://github.com/ant-design/ant-design/issues/6717 + // Don't bind keyboard support when children specify the onKeyDown + + if (children && children.props.onKeyDown) { + children.props.onKeyDown(e); + return; + } + var activeValue = [].concat(_toConsumableArray(_this.state.activeValue)); + var currentLevel = activeValue.length - 1 < 0 ? 0 : activeValue.length - 1; + var currentOptions = _this.getCurrentLevelOptions(); + var currentIndex = currentOptions + .map(function(o) { + return o[_this.getFieldName('value')]; + }) + .indexOf(activeValue[currentLevel]); + if ( + e.keyCode !== KeyCode.DOWN && + e.keyCode !== KeyCode.UP && + e.keyCode !== KeyCode.LEFT && + e.keyCode !== KeyCode.RIGHT && + e.keyCode !== KeyCode.ENTER && + e.keyCode !== KeyCode.BACKSPACE && + e.keyCode !== KeyCode.ESC + ) { + return; + } + // Press any keys above to reopen menu + if ( + !_this.state.popupVisible && + e.keyCode !== KeyCode.BACKSPACE && + e.keyCode !== KeyCode.LEFT && + e.keyCode !== KeyCode.RIGHT && + e.keyCode !== KeyCode.ESC + ) { + _this.setPopupVisible(true); + return; + } + if (e.keyCode === KeyCode.DOWN || e.keyCode === KeyCode.UP) { + e.preventDefault(); + var nextIndex = currentIndex; + if (nextIndex !== -1) { + if (e.keyCode === KeyCode.DOWN) { + nextIndex += 1; + nextIndex = nextIndex >= currentOptions.length ? 0 : nextIndex; + } else { + nextIndex -= 1; + nextIndex = nextIndex < 0 ? currentOptions.length - 1 : nextIndex; + } + } else { + nextIndex = 0; + } + activeValue[currentLevel] = currentOptions[nextIndex][_this.getFieldName('value')]; + } else if (e.keyCode === KeyCode.LEFT || e.keyCode === KeyCode.BACKSPACE) { + e.preventDefault(); + activeValue.splice(activeValue.length - 1, 1); + } else if (e.keyCode === KeyCode.RIGHT) { + e.preventDefault(); + if ( + currentOptions[currentIndex] && + currentOptions[currentIndex][_this.getFieldName('children')] + ) { + activeValue.push( + currentOptions[currentIndex][_this.getFieldName('children')][0][ + _this.getFieldName('value') + ], + ); + } + } else if (e.keyCode === KeyCode.ESC) { + _this.setPopupVisible(false); + return; + } + if (!activeValue || activeValue.length === 0) { + _this.setPopupVisible(false); + } + var activeOptions = _this.getActiveOptions(activeValue); + var targetOption = activeOptions[activeOptions.length - 1]; + _this.handleMenuSelect(targetOption, activeOptions.length - 1, e); + + if (_this.props.onKeyDown) { + _this.props.onKeyDown(e); + } + }; + + _this.saveTrigger = function(node) { + _this.trigger = node; + }; + + var initialValue = []; + if ('value' in props) { + initialValue = props.value || []; + } else if ('defaultValue' in props) { + initialValue = props.defaultValue || []; + } + + warning( + !('filedNames' in props), + '`filedNames` of Cascader is a typo usage and deprecated, please use `fieldNames` instead.', + ); + + _this.state = { + popupVisible: props.popupVisible, + activeValue: initialValue, + value: initialValue, + prevProps: props, + }; + _this.defaultFieldNames = { label: 'label', value: 'value', children: 'children' }; + return _this; + } + + _createClass( + Cascader, + [ + { + key: 'getPopupDOMNode', + value: function getPopupDOMNode() { + return this.trigger.getPopupDomNode(); + }, + }, + { + key: 'getFieldName', + value: function getFieldName(name) { + var defaultFieldNames = this.defaultFieldNames; + var _props = this.props, + fieldNames = _props.fieldNames, + filedNames = _props.filedNames; + + if ('filedNames' in this.props) { + return filedNames[name] || defaultFieldNames[name]; // For old compatibility + } + return fieldNames[name] || defaultFieldNames[name]; + }, + }, + { + key: 'getFieldNames', + value: function getFieldNames() { + var _props2 = this.props, + fieldNames = _props2.fieldNames, + filedNames = _props2.filedNames; + + if ('filedNames' in this.props) { + return filedNames; // For old compatibility + } + return fieldNames; + }, + }, + { + key: 'getCurrentLevelOptions', + value: function getCurrentLevelOptions() { + var _this2 = this; + + var _props$options = this.props.options, + options = _props$options === undefined ? [] : _props$options; + var _state$activeValue = this.state.activeValue, + activeValue = _state$activeValue === undefined ? [] : _state$activeValue; + + var result = arrayTreeFilter( + options, + function(o, level) { + return o[_this2.getFieldName('value')] === activeValue[level]; + }, + { childrenKeyName: this.getFieldName('children') }, + ); + if (result[result.length - 2]) { + return result[result.length - 2][this.getFieldName('children')]; + } + return [].concat(_toConsumableArray(options)).filter(function(o) { + return !o.disabled; + }); + }, + }, + { + key: 'getActiveOptions', + value: function getActiveOptions(activeValue) { + var _this3 = this; + + return arrayTreeFilter( + this.props.options || [], + function(o, level) { + return o[_this3.getFieldName('value')] === activeValue[level]; + }, + { childrenKeyName: this.getFieldName('children') }, + ); + }, + }, + { + key: 'render', + value: function render() { + var _props3 = this.props, + prefixCls = _props3.prefixCls, + transitionName = _props3.transitionName, + popupClassName = _props3.popupClassName, + _props3$options = _props3.options, + options = _props3$options === undefined ? [] : _props3$options, + disabled = _props3.disabled, + builtinPlacements = _props3.builtinPlacements, + popupPlacement = _props3.popupPlacement, + children = _props3.children, + restProps = _objectWithoutProperties(_props3, [ + 'prefixCls', + 'transitionName', + 'popupClassName', + 'options', + 'disabled', + 'builtinPlacements', + 'popupPlacement', + 'children', + ]); + // Did not show popup when there is no options + + var menus = React.createElement('div', null); + var emptyMenuClassName = ''; + if (options && options.length > 0) { + menus = React.createElement( + Menus, + _extends({}, this.props, { + fieldNames: this.getFieldNames(), + defaultFieldNames: this.defaultFieldNames, + activeValue: this.state.activeValue, + onSelect: this.handleMenuSelect, + onItemDoubleClick: this.handleItemDoubleClick, + visible: this.state.popupVisible, + }), + ); + } else { + emptyMenuClassName = ' ' + prefixCls + '-menus-empty'; + } + return React.createElement( + Trigger, + _extends( + { + ref: this.saveTrigger, + }, + restProps, + { + options: options, + disabled: disabled, + popupPlacement: popupPlacement, + builtinPlacements: builtinPlacements, + popupTransitionName: transitionName, + action: disabled ? [] : ['click'], + popupVisible: disabled ? false : this.state.popupVisible, + onPopupVisibleChange: this.handlePopupVisibleChange, + prefixCls: prefixCls + '-menus', + popupClassName: popupClassName + emptyMenuClassName, + popup: menus, + }, + ), + cloneElement(children, { + onKeyDown: this.handleKeyDown, + tabIndex: disabled ? undefined : 0, + }), + ); + }, + }, + ], + [ + { + key: 'getDerivedStateFromProps', + value: function getDerivedStateFromProps(nextProps, prevState) { + var _prevState$prevProps = prevState.prevProps, + prevProps = _prevState$prevProps === undefined ? {} : _prevState$prevProps; + + var newState = { + prevProps: nextProps, + }; + + if ('value' in nextProps && !shallowEqualArrays(prevProps.value, nextProps.value)) { + newState.value = nextProps.value || []; + + // allow activeValue diff from value + // https://github.com/ant-design/ant-design/issues/2767 + if (!('loadData' in nextProps)) { + newState.activeValue = nextProps.value || []; + } + } + if ('popupVisible' in nextProps) { + newState.popupVisible = nextProps.popupVisible; + } + + return newState; + }, + }, + ], + ); + + return Cascader; +})(Component); + +Cascader.defaultProps = { + onChange: function onChange() {}, + onPopupVisibleChange: function onPopupVisibleChange() {}, + + disabled: false, + transitionName: '', + prefixCls: 'rc-cascader', + popupClassName: '', + popupPlacement: 'bottomLeft', + builtinPlacements: BUILT_IN_PLACEMENTS, + expandTrigger: 'click', + fieldNames: { label: 'label', value: 'value', children: 'children' }, + expandIcon: '>', +}; + +Cascader.propTypes = { + value: PropTypes.array, + defaultValue: PropTypes.array, + options: PropTypes.array.isRequired, + onChange: PropTypes.func, + onPopupVisibleChange: PropTypes.func, + popupVisible: PropTypes.bool, + disabled: PropTypes.bool, + transitionName: PropTypes.string, + popupClassName: PropTypes.string, + popupPlacement: PropTypes.string, + prefixCls: PropTypes.string, + dropdownMenuColumnStyle: PropTypes.object, + builtinPlacements: PropTypes.object, + loadData: PropTypes.func, + changeOnSelect: PropTypes.bool, + children: PropTypes.node, + onKeyDown: PropTypes.func, + expandTrigger: PropTypes.string, + fieldNames: PropTypes.object, + filedNames: PropTypes.object, // typo but for compatibility + expandIcon: PropTypes.node, + loadingIcon: PropTypes.node, +}; + +polyfill(Cascader); + +export default Cascader; diff --git a/es/Menus.js b/es/Menus.js new file mode 100644 index 00000000..fb0ae814 --- /dev/null +++ b/es/Menus.js @@ -0,0 +1,268 @@ +import _extends from 'babel-runtime/helpers/extends'; +import _classCallCheck from 'babel-runtime/helpers/classCallCheck'; +import _createClass from 'babel-runtime/helpers/createClass'; +import _possibleConstructorReturn from 'babel-runtime/helpers/possibleConstructorReturn'; +import _inherits from 'babel-runtime/helpers/inherits'; +import React from 'react'; +import PropTypes from 'prop-types'; +import arrayTreeFilter from 'array-tree-filter'; +import { findDOMNode } from 'react-dom'; + +var Menus = (function(_React$Component) { + _inherits(Menus, _React$Component); + + function Menus(props) { + _classCallCheck(this, Menus); + + var _this = _possibleConstructorReturn( + this, + (Menus.__proto__ || Object.getPrototypeOf(Menus)).call(this, props), + ); + + _this.saveMenuItem = function(index) { + return function(node) { + _this.menuItems[index] = node; + }; + }; + + _this.menuItems = {}; + return _this; + } + + _createClass(Menus, [ + { + key: 'componentDidMount', + value: function componentDidMount() { + this.scrollActiveItemToView(); + }, + }, + { + key: 'componentDidUpdate', + value: function componentDidUpdate(prevProps) { + if (!prevProps.visible && this.props.visible) { + this.scrollActiveItemToView(); + } + }, + }, + { + key: 'getFieldName', + value: function getFieldName(name) { + var _props = this.props, + fieldNames = _props.fieldNames, + defaultFieldNames = _props.defaultFieldNames; + // 防止只设置单个属性的名字 + + return fieldNames[name] || defaultFieldNames[name]; + }, + }, + { + key: 'getOption', + value: function getOption(option, menuIndex) { + var _props2 = this.props, + prefixCls = _props2.prefixCls, + expandTrigger = _props2.expandTrigger, + expandIcon = _props2.expandIcon, + loadingIcon = _props2.loadingIcon; + + var onSelect = this.props.onSelect.bind(this, option, menuIndex); + var onItemDoubleClick = this.props.onItemDoubleClick.bind(this, option, menuIndex); + var expandProps = { + onClick: onSelect, + onDoubleClick: onItemDoubleClick, + }; + var menuItemCls = prefixCls + '-menu-item'; + var expandIconNode = null; + var hasChildren = + option[this.getFieldName('children')] && option[this.getFieldName('children')].length > 0; + if (hasChildren || option.isLeaf === false) { + menuItemCls += ' ' + prefixCls + '-menu-item-expand'; + if (!option.loading) { + expandIconNode = React.createElement( + 'span', + { className: prefixCls + '-menu-item-expand-icon' }, + expandIcon, + ); + } + } + if (expandTrigger === 'hover' && hasChildren) { + expandProps = { + onMouseEnter: this.delayOnSelect.bind(this, onSelect), + onMouseLeave: this.delayOnSelect.bind(this), + onClick: onSelect, + }; + } + if (this.isActiveOption(option, menuIndex)) { + menuItemCls += ' ' + prefixCls + '-menu-item-active'; + expandProps.ref = this.saveMenuItem(menuIndex); + } + if (option.disabled) { + menuItemCls += ' ' + prefixCls + '-menu-item-disabled'; + } + + var loadingIconNode = null; + if (option.loading) { + menuItemCls += ' ' + prefixCls + '-menu-item-loading'; + loadingIconNode = loadingIcon || null; + } + var title = ''; + if (option.title) { + title = option.title; + } else if (typeof option[this.getFieldName('label')] === 'string') { + title = option[this.getFieldName('label')]; + } + + return React.createElement( + 'li', + _extends( + { + key: option[this.getFieldName('value')], + className: menuItemCls, + title: title, + }, + expandProps, + ), + option[this.getFieldName('label')], + expandIconNode, + loadingIconNode, + ); + }, + }, + { + key: 'getActiveOptions', + value: function getActiveOptions(values) { + var _this2 = this; + + var activeValue = values || this.props.activeValue; + var options = this.props.options; + return arrayTreeFilter( + options, + function(o, level) { + return o[_this2.getFieldName('value')] === activeValue[level]; + }, + { childrenKeyName: this.getFieldName('children') }, + ); + }, + }, + { + key: 'getShowOptions', + value: function getShowOptions() { + var _this3 = this; + + var options = this.props.options; + + var result = this.getActiveOptions() + .map(function(activeOption) { + return activeOption[_this3.getFieldName('children')]; + }) + .filter(function(activeOption) { + return !!activeOption && Array.isArray(activeOption) && activeOption.length > 0; + }); + result.unshift(options); + return result; + }, + }, + { + key: 'delayOnSelect', + value: function delayOnSelect(onSelect) { + var _this4 = this; + + for ( + var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; + _key < _len; + _key++ + ) { + args[_key - 1] = arguments[_key]; + } + + if (this.delayTimer) { + clearTimeout(this.delayTimer); + this.delayTimer = null; + } + if (typeof onSelect === 'function') { + this.delayTimer = setTimeout(function() { + onSelect(args); + _this4.delayTimer = null; + }, 150); + } + }, + }, + { + key: 'scrollActiveItemToView', + value: function scrollActiveItemToView() { + // scroll into view + var optionsLength = this.getShowOptions().length; + for (var i = 0; i < optionsLength; i++) { + var itemComponent = this.menuItems[i]; + if (itemComponent) { + var target = findDOMNode(itemComponent); + target.parentNode.scrollTop = target.offsetTop; + } + } + }, + }, + { + key: 'isActiveOption', + value: function isActiveOption(option, menuIndex) { + var _props$activeValue = this.props.activeValue, + activeValue = _props$activeValue === undefined ? [] : _props$activeValue; + + return activeValue[menuIndex] === option[this.getFieldName('value')]; + }, + }, + { + key: 'render', + value: function render() { + var _this5 = this; + + var _props3 = this.props, + prefixCls = _props3.prefixCls, + dropdownMenuColumnStyle = _props3.dropdownMenuColumnStyle; + + return React.createElement( + 'div', + null, + this.getShowOptions().map(function(options, menuIndex) { + return React.createElement( + 'ul', + { className: prefixCls + '-menu', key: menuIndex, style: dropdownMenuColumnStyle }, + options.map(function(option) { + return _this5.getOption(option, menuIndex); + }), + ); + }), + ); + }, + }, + ]); + + return Menus; +})(React.Component); + +Menus.defaultProps = { + options: [], + value: [], + activeValue: [], + onSelect: function onSelect() {}, + + prefixCls: 'rc-cascader-menus', + visible: false, + expandTrigger: 'click', +}; + +Menus.propTypes = { + value: PropTypes.array, + activeValue: PropTypes.array, + options: PropTypes.array, + prefixCls: PropTypes.string, + expandTrigger: PropTypes.string, + onSelect: PropTypes.func, + visible: PropTypes.bool, + dropdownMenuColumnStyle: PropTypes.object, + defaultFieldNames: PropTypes.object, + fieldNames: PropTypes.object, + expandIcon: PropTypes.node, + loadingIcon: PropTypes.node, + onItemDoubleClick: PropTypes.func, +}; + +export default Menus; diff --git a/es/index.js b/es/index.js new file mode 100644 index 00000000..77fa5d1c --- /dev/null +++ b/es/index.js @@ -0,0 +1,4 @@ +// export this package's api +import Cascader from './Cascader'; + +export default Cascader; diff --git a/package.json b/package.json index 220c5625..bcad4ae6 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ }, "files": [ "lib", - "es", "assets/*.css" ], "licenses": "MIT", @@ -39,7 +38,6 @@ "pub": "rc-tools run pub", "lint": "rc-tools run lint", "test": "jest", - "pre-commit": "rc-tools run pre-commit", "prettier": "rc-tools run prettier", "lint-staged": "lint-staged", "coverage": "jest --coverage" @@ -70,9 +68,6 @@ "react": "^16.0.0", "react-dom": "^16.0.0" }, - "pre-commit": [ - "lint-staged" - ], "dependencies": { "array-tree-filter": "^2.1.0", "prop-types": "^15.5.8", diff --git a/src/Menus.jsx b/src/Menus.jsx index c07867c3..47db51a0 100644 --- a/src/Menus.jsx +++ b/src/Menus.jsx @@ -97,7 +97,9 @@ class Menus extends React.Component { const { options } = this.props; const result = this.getActiveOptions() .map(activeOption => activeOption[this.getFieldName('children')]) - .filter(activeOption => !!activeOption); + .filter(activeOption => { + return !!activeOption && Array.isArray(activeOption) && activeOption.length > 0; + }); result.unshift(options); return result; } @@ -173,6 +175,7 @@ Menus.propTypes = { fieldNames: PropTypes.object, expandIcon: PropTypes.node, loadingIcon: PropTypes.node, + onItemDoubleClick: PropTypes.func, }; export default Menus;