UNPKG

25.6 kBJavaScriptView Raw
1import _extends from 'babel-runtime/helpers/extends';
2import _classCallCheck from 'babel-runtime/helpers/classCallCheck';
3import _possibleConstructorReturn from 'babel-runtime/helpers/possibleConstructorReturn';
4import _inherits from 'babel-runtime/helpers/inherits';
5
6var _class, _temp;
7
8import React, { Children } from 'react';
9import { findDOMNode } from 'react-dom';
10import PropTypes from 'prop-types';
11import classNames from 'classnames';
12import { func, dom, events } from '../util';
13import Menu from '../menu';
14import Overlay from '../overlay';
15import Input from '../input';
16
17import zhCN from '../locale/zh-cn';
18import DataStore from './data-store';
19import VirtualList from '../virtual-list';
20import { isSingle, filter, isNull, valueToSelectKey, getValueDataSource } from './util';
21
22var Popup = Overlay.Popup;
23var MenuItem = Menu.Item,
24 MenuGroup = Menu.Group;
25var noop = func.noop,
26 bindCtx = func.bindCtx,
27 makeChain = func.makeChain;
28
29
30function preventDefault(e) {
31 e.preventDefault();
32}
33
34var Base = (_temp = _class = function (_React$Component) {
35 _inherits(Base, _React$Component);
36
37 function Base(props) {
38 _classCallCheck(this, Base);
39
40 var _this = _possibleConstructorReturn(this, _React$Component.call(this, props));
41
42 _this.handleMouseDown = function (e) {
43 if (!_this.props.popupAutoFocus) {
44 preventDefault(e);
45 }
46 };
47
48 _this.saveSelectRef = function (ref) {
49 _this.selectDOM = findDOMNode(ref);
50 };
51
52 _this.saveInputRef = function (ref) {
53 if (ref && ref.getInstance()) {
54 _this.inputRef = ref.getInstance();
55 }
56 };
57
58 _this.savePopupRef = function (ref) {
59 _this.popupRef = ref;
60 };
61
62 _this.dataStore = new DataStore({
63 filter: props.filter,
64 filterLocal: props.filterLocal,
65 showDataSourceChildren: props.showDataSourceChildren
66 });
67
68 var mode = props.mode;
69
70 var value = 'value' in props ? props.value : props.defaultValue;
71
72 // 多选情况下做 value 数组订正
73 if (props.mode !== 'single' && value && !Array.isArray(value)) {
74 value = [value];
75 }
76
77 _this.state = {
78 dataStore: _this.dataStore,
79 value: value,
80 visible: 'visible' in props ? props.visible : props.defaultVisible,
81 dataSource: _this.setDataSource(_this.props),
82 width: 100,
83 // highlightKey应为String 多选初始化只赋值受控highlightKey/defaultHighlightKey
84 highlightKey: 'highlightKey' in props ? props.highlightKey : props.mode === 'single' ? props.value || props.defaultHighlightKey || props.defaultValue : props.defaultHighlightKey,
85 srReader: ''
86 };
87
88 bindCtx(_this, ['handleMenuBodyClick', 'handleVisibleChange', 'focusInput', 'beforeOpen', 'beforeClose', 'afterClose', 'handleResize']);
89 return _this;
90 }
91
92 Base.prototype.componentDidMount = function componentDidMount() {
93 var _this2 = this;
94
95 // overlay 还没有完成 mount,所以需要滞后同步宽度
96 setTimeout(function () {
97 return _this2.syncWidth();
98 }, 0);
99
100 events.on(window, 'resize', this.handleResize);
101 };
102
103 Base.prototype.componentDidUpdate = function componentDidUpdate(prevProps, prevState) {
104 if (prevProps.label !== this.props.label || prevState.value !== this.state.value) {
105 this.syncWidth();
106 }
107 };
108
109 Base.prototype.componentWillUnmount = function componentWillUnmount() {
110 events.off(window, 'resize', this.handleResize);
111 clearTimeout(this.resizeTimeout);
112 };
113
114 /**
115 * Calculate and set width of popup menu
116 * @protected
117 */
118
119
120 Base.prototype.syncWidth = function syncWidth() {
121 var _this3 = this;
122
123 var _props2 = this.props,
124 popupStyle = _props2.popupStyle,
125 popupProps = _props2.popupProps;
126
127 if (popupStyle && 'width' in popupStyle || popupProps && popupProps.style && 'width' in popupProps.style) {
128 return;
129 }
130
131 var width = dom.getStyle(this.selectDOM, 'width');
132 if (width && this.width !== width) {
133 this.width = width;
134
135 if (this.popupRef && this.shouldAutoWidth()) {
136 // overy 的 node 节点可能没有挂载完成,所以这里需要异步
137 setTimeout(function () {
138 if (_this3.popupRef) {
139 dom.setStyle(_this3.popupRef, 'width', _this3.width);
140 return;
141 }
142 }, 0);
143 }
144 }
145 };
146
147 Base.prototype.handleResize = function handleResize() {
148 var _this4 = this;
149
150 clearTimeout(this.resizeTimeout);
151 if (this.state.visible) {
152 this.resizeTimeout = setTimeout(function () {
153 _this4.syncWidth();
154 }, 200);
155 }
156 };
157
158 /**
159 * Get structured dataSource, for cache
160 * @protected
161 * @param {Object} [props=this.props]
162 * @return {Array}
163 */
164
165
166 Base.prototype.setDataSource = function setDataSource(props) {
167 var dataSource = props.dataSource,
168 children = props.children;
169
170 // children is higher priority then dataSource
171
172 if (Children.count(children)) {
173 return this.dataStore.updateByDS(children, true);
174 } else if (Array.isArray(dataSource)) {
175 return this.dataStore.updateByDS(dataSource, false);
176 }
177 return [];
178 };
179
180 /**
181 * Set popup visible
182 * @protected
183 * @param {boolean} visible
184 * @param {string} type trigger type
185 */
186
187
188 Base.prototype.setVisible = function setVisible(visible, type) {
189 // disabled 状态下只允许关闭不允许打开
190 if (this.props.disabled && visible || this.state.visible === visible) {
191 return;
192 }
193
194 if (!('visible' in this.props)) {
195 this.setState({
196 visible: visible
197 });
198 }
199
200 this.props.onVisibleChange(visible, type);
201 };
202
203 Base.prototype.setFirstHightLightKeyForMenu = function setFirstHightLightKeyForMenu(searchValue) {
204 // 判断value/highlightKey解决受控后,默认高亮第一个元素问题。(当搜索值时,搜索后应执行默认选择第一个元素)
205 var highlightKey = this.state.highlightKey;
206
207 if (!this.props.autoHighlightFirstItem) {
208 return;
209 }
210
211 // 设置高亮 item key
212 if (this.dataStore.getMenuDS().length && this.dataStore.getEnableDS().length && (!highlightKey || searchValue)) {
213 var _highlightKey = '' + this.dataStore.getEnableDS()[0].value;
214 this.setState({
215 highlightKey: _highlightKey
216 });
217 this.props.onToggleHighlightItem(_highlightKey, 'autoFirstItem');
218 }
219
220 // 当有搜索值且搜索条件与dataSource不匹配时(搜索条件不满足不会出现可选择的列表,所以高亮key应为null)
221 if (searchValue && !this.dataStore.getEnableDS().length) {
222 this.setState({
223 highlightKey: null
224 });
225 this.props.onToggleHighlightItem(null, 'highlightKeyToNull');
226 }
227 };
228
229 Base.prototype.handleChange = function handleChange(value) {
230 var _props3;
231
232 // 非受控模式清空内部数据
233 if (!('value' in this.props)) {
234 this.setState({
235 value: value
236 });
237 }
238
239 for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
240 args[_key - 1] = arguments[_key];
241 }
242
243 (_props3 = this.props).onChange.apply(_props3, [value].concat(args));
244 };
245
246 /**
247 * Handle Menu body click
248 * @param {Event} e click event
249 */
250
251
252 Base.prototype.handleMenuBodyClick = function handleMenuBodyClick(e) {
253 if (!this.props.popupAutoFocus) {
254 this.focusInput(e);
255 }
256 };
257
258 /**
259 * Toggle highlight MenuItem
260 * @private
261 * @param {number} dir -1: up, 1: down
262 */
263
264
265 Base.prototype.toggleHighlightItem = function toggleHighlightItem(dir) {
266 if (!this.state.visible) {
267 this.setVisible(true, 'enter');
268 return;
269 }
270
271 var maxCount = this.dataStore.getEnableDS().length;
272 // When there is no enabled item
273 if (!maxCount) {
274 return false;
275 }
276
277 var highlightKey = this.state.highlightKey;
278
279 var highlightIndex = -1;
280
281 // find previous highlight index
282 highlightKey !== null && this.dataStore.getEnableDS().some(function (item, index) {
283 if ('' + item.value === highlightKey) {
284 highlightIndex = index;
285 }
286 return highlightIndex > -1;
287 });
288
289 // toggle highlight index
290 highlightIndex += dir;
291 if (highlightIndex < 0) {
292 highlightIndex = maxCount - 1;
293 }
294 if (highlightIndex >= maxCount) {
295 highlightIndex = 0;
296 }
297
298 // get highlight key
299 var highlightItem = this.dataStore.getEnableDS()[highlightIndex];
300 highlightKey = highlightItem ? '' + highlightItem.value : null;
301
302 this.setState({ highlightKey: highlightKey, srReader: highlightItem.label });
303
304 this.scrollMenuIntoView();
305
306 return highlightItem;
307 };
308
309 // scroll into focus item
310
311
312 Base.prototype.scrollMenuIntoView = function scrollMenuIntoView() {
313 var _this5 = this;
314
315 var prefix = this.props.prefix;
316
317
318 clearTimeout(this.highlightTimer);
319 this.highlightTimer = setTimeout(function () {
320 try {
321 var menuNode = findDOMNode(_this5.menuRef);
322 var itemNode = menuNode.querySelector('.' + prefix + 'select-menu-item.' + prefix + 'focused');
323 itemNode && itemNode.scrollIntoViewIfNeeded && itemNode.scrollIntoViewIfNeeded();
324 } catch (ex) {
325 // I don't care...
326 }
327 });
328 };
329
330 /**
331 * render popup menu header
332 * @abstract
333 */
334
335
336 Base.prototype.renderMenuHeader = function renderMenuHeader() {
337 var menuProps = this.props.menuProps;
338
339
340 if (menuProps && 'header' in menuProps) {
341 return menuProps.header;
342 }
343
344 return null;
345 };
346
347 Base.prototype.handleSelect = function handleSelect() {};
348
349 /**
350 * 防止 onBlur/onFocus 抖动
351 */
352
353 /**
354 * render popup children
355 * @protected
356 * @param {object} props
357 */
358 Base.prototype.renderMenu = function renderMenu() {
359 var _classNames,
360 _this6 = this;
361
362 var _props4 = this.props,
363 prefix = _props4.prefix,
364 mode = _props4.mode,
365 locale = _props4.locale,
366 notFoundContent = _props4.notFoundContent,
367 useVirtual = _props4.useVirtual,
368 menuProps = _props4.menuProps;
369 var _state = this.state,
370 dataSource = _state.dataSource,
371 highlightKey = _state.highlightKey;
372
373 var value = this.state.value;
374 var selectedKeys = void 0;
375
376 if (isNull(value) || value.length === 0 || this.isAutoComplete) {
377 selectedKeys = [];
378 } else if (isSingle(mode)) {
379 selectedKeys = [valueToSelectKey(value)];
380 } else {
381 selectedKeys = [].concat(value).map(function (n) {
382 return valueToSelectKey(n);
383 });
384 }
385
386 var children = this.renderMenuItem(dataSource);
387
388 var menuClassName = classNames((_classNames = {}, _classNames[prefix + 'select-menu'] = true, _classNames[prefix + 'select-menu-empty'] = !children || !children.length, _classNames));
389
390 if (!children || !children.length) {
391 children = React.createElement(
392 'span',
393 { className: prefix + 'select-menu-empty-content' },
394 notFoundContent || locale.notFoundContent
395 );
396 }
397
398 var customProps = _extends({}, menuProps, {
399 children: children,
400 role: 'listbox',
401 selectedKeys: selectedKeys,
402 focusedKey: highlightKey,
403 focusable: false,
404 selectMode: isSingle(mode) ? 'single' : 'multiple',
405 onSelect: this.handleMenuSelect,
406 onItemClick: this.handleItemClick,
407 header: this.renderMenuHeader(),
408 onClick: this.handleMenuBodyClick,
409 onMouseDown: this.handleMouseDown,
410 className: menuClassName
411 });
412 var menuStyle = this.shouldAutoWidth() ? { width: '100%' } : { minWidth: this.width };
413
414 return useVirtual && children.length > 10 ? React.createElement(
415 'div',
416 { className: prefix + 'select-menu-wrapper', style: _extends({ position: 'relative' }, menuStyle) },
417 React.createElement(
418 VirtualList,
419 {
420 itemsRenderer: function itemsRenderer(items, _ref) {
421 return React.createElement(
422 Menu,
423 _extends({
424 ref: function ref(c) {
425 _ref(c);
426 _this6.menuRef = c;
427 },
428 flatenContent: true
429 }, customProps),
430 items
431 );
432 }
433 },
434 children
435 )
436 ) : React.createElement(Menu, _extends({}, customProps, { style: menuStyle }));
437 };
438
439 /**
440 * render menu item
441 * @protected
442 * @param {Array} dataSource
443 */
444
445
446 Base.prototype.renderMenuItem = function renderMenuItem(dataSource) {
447 var _this7 = this;
448
449 var _props5 = this.props,
450 prefix = _props5.prefix,
451 itemRender = _props5.itemRender,
452 showDataSourceChildren = _props5.showDataSourceChildren;
453 // If it has.
454
455 var searchKey = void 0;
456 if (this.isAutoComplete) {
457 // In AutoComplete, value is the searchKey
458 searchKey = this.state.value;
459 } else {
460 searchKey = this.state.searchValue;
461 }
462
463 return dataSource.map(function (item, index) {
464 if (!item) {
465 return null;
466 }
467 if (Array.isArray(item.children) && showDataSourceChildren) {
468 return React.createElement(
469 MenuGroup,
470 { key: index, label: item.label },
471 _this7.renderMenuItem(item.children)
472 );
473 } else {
474 var itemProps = {
475 role: 'option',
476 key: item.value,
477 className: prefix + 'select-menu-item',
478 disabled: item.disabled
479 };
480
481 if ('title' in item) {
482 itemProps.title = item.title;
483 }
484
485 return React.createElement(
486 MenuItem,
487 itemProps,
488 itemRender(item, searchKey)
489 );
490 }
491 });
492 };
493
494 /**
495 * 点击 arrow 或 label 的时候焦点切到 input 中
496 * @override
497 */
498 Base.prototype.focusInput = function focusInput() {
499 this.inputRef.focus();
500 };
501
502 Base.prototype.focus = function focus() {
503 var _inputRef;
504
505 (_inputRef = this.inputRef).focus.apply(_inputRef, arguments);
506 };
507
508 Base.prototype.beforeOpen = function beforeOpen() {
509 if (this.props.mode === 'single') {
510 this.setFirstHightLightKeyForMenu();
511 }
512 this.syncWidth();
513 };
514
515 Base.prototype.beforeClose = function beforeClose() {};
516
517 Base.prototype.afterClose = function afterClose() {};
518
519 Base.prototype.shouldAutoWidth = function shouldAutoWidth() {
520 if (this.props.popupComponent) {
521 return false;
522 }
523
524 return this.props.autoWidth;
525 };
526
527 Base.prototype.render = function render(props) {
528 var _classNames2;
529
530 var prefix = props.prefix,
531 mode = props.mode,
532 popupProps = props.popupProps,
533 popupContainer = props.popupContainer,
534 popupClassName = props.popupClassName,
535 popupStyle = props.popupStyle,
536 popupContent = props.popupContent,
537 canCloseByTrigger = props.canCloseByTrigger,
538 followTrigger = props.followTrigger,
539 cache = props.cache,
540 popupComponent = props.popupComponent,
541 isPreview = props.isPreview,
542 renderPreview = props.renderPreview,
543 style = props.style,
544 className = props.className;
545
546
547 var cls = classNames((_classNames2 = {}, _classNames2[prefix + 'select-auto-complete-menu'] = !popupContent && this.isAutoComplete, _classNames2[prefix + 'select-' + mode + '-menu'] = !popupContent && !!mode, _classNames2), popupClassName || popupProps.className);
548
549 if (isPreview) {
550 if (this.isAutoComplete) {
551 return React.createElement(Input, {
552 style: style,
553 className: className,
554 isPreview: isPreview,
555 renderPreview: renderPreview,
556 value: this.state.value
557 });
558 } else {
559 var value = this.state.value;
560 var valueDS = this.state.value;
561
562 if (!this.useDetailValue()) {
563 if (value === this.valueDataSource.value) {
564 valueDS = this.valueDataSource.valueDS;
565 } else {
566 valueDS = getValueDataSource(value, this.valueDataSource.mapValueDS, this.dataStore.getMapDS()).valueDS;
567 }
568 }
569
570 if (typeof renderPreview === 'function') {
571 var _classNames3;
572
573 var previewCls = classNames((_classNames3 = {}, _classNames3[prefix + 'form-preview'] = true, _classNames3[className] = !!className, _classNames3));
574 return React.createElement(
575 'div',
576 { style: style, className: previewCls },
577 renderPreview(valueDS, this.props)
578 );
579 } else {
580 var fillProps = this.props.fillProps;
581
582 if (mode === 'single') {
583 return React.createElement(Input, {
584 style: style,
585 className: className,
586 isPreview: isPreview,
587 value: valueDS ? fillProps ? valueDS[fillProps] : valueDS.label : ''
588 });
589 } else {
590 return React.createElement(Input, {
591 style: style,
592 className: className,
593 isPreview: isPreview,
594 value: (valueDS || []).map(function (i) {
595 return i.label;
596 }).join(', ')
597 });
598 }
599 }
600 }
601 }
602
603 var _props = _extends({
604 triggerType: 'click',
605 autoFocus: !!this.props.popupAutoFocus,
606 cache: cache
607 }, popupProps, {
608 //beforeOpen node not mount, afterOpen too slow.
609 // from display:none to block, we may need to recompute width
610 beforeOpen: makeChain(this.beforeOpen, popupProps.beforeOpen),
611 beforeClose: makeChain(this.beforeClose, popupProps.beforeClose),
612 afterClose: makeChain(this.afterClose, popupProps.afterClose),
613 canCloseByTrigger: canCloseByTrigger,
614 followTrigger: followTrigger,
615 visible: this.state.visible,
616 onVisibleChange: this.handleVisibleChange,
617 shouldUpdatePosition: true,
618 container: popupContainer || popupProps.container,
619 className: cls,
620 style: popupStyle || popupProps.style
621 });
622
623 if (popupProps.v2) {
624 delete _props.shouldUpdatePosition;
625 }
626
627 var Tag = popupComponent ? popupComponent : Popup;
628
629 return React.createElement(
630 Tag,
631 _extends({}, _props, { trigger: this.renderSelect() }),
632 popupContent ? React.createElement(
633 'div',
634 {
635 className: prefix + 'select-popup-wrap',
636 style: this.shouldAutoWidth() ? { width: this.width } : {},
637 ref: this.savePopupRef
638 },
639 popupContent
640 ) : React.createElement(
641 'div',
642 {
643 className: prefix + 'select-spacing-tb',
644 style: this.shouldAutoWidth() ? { width: this.width } : {},
645 ref: this.savePopupRef
646 },
647 this.renderMenu()
648 )
649 );
650 };
651
652 return Base;
653}(React.Component), _class.propTypes = {
654 prefix: PropTypes.string,
655 /**
656 * 选择器尺寸
657 */
658 size: PropTypes.oneOf(['small', 'medium', 'large']),
659 // 当前值,用于受控模式
660 value: PropTypes.any, // to be override
661 // 初始化的默认值
662 defaultValue: PropTypes.any, // to be override
663 /**
664 * 没有值的时候的占位符
665 */
666 placeholder: PropTypes.string,
667 /**
668 * 下拉菜单是否与选择器对齐
669 */
670 autoWidth: PropTypes.bool,
671 /**
672 * 自定义内联 label
673 */
674 label: PropTypes.node,
675 /**
676 * 是否有清除按钮(单选模式有效)
677 */
678 hasClear: PropTypes.bool,
679 /**
680 * 校验状态
681 */
682 state: PropTypes.oneOf(['error', 'loading', 'success', 'warning']),
683 /**
684 * 是否只读,只读模式下可以展开弹层但不能选
685 */
686 readOnly: PropTypes.bool,
687 /**
688 * 是否禁用选择器
689 */
690 disabled: PropTypes.bool,
691 /**
692 * 当前弹层是否显示
693 */
694 visible: PropTypes.bool,
695 /**
696 * 弹层初始化是否显示
697 */
698 defaultVisible: PropTypes.bool,
699 /**
700 * 弹层显示或隐藏时触发的回调
701 * @param {Boolean} visible 弹层是否显示
702 * @param {String} type 触发弹层显示或隐藏的来源 fromContent 表示由Dropdown内容触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发
703 */
704 onVisibleChange: PropTypes.func,
705 /**
706 * 弹层挂载的容器节点
707 */
708 popupContainer: PropTypes.any,
709 /**
710 * 弹层的 className
711 */
712 popupClassName: PropTypes.any,
713 /**
714 * 弹层的内联样式
715 */
716 popupStyle: PropTypes.object,
717 /**
718 * 添加到弹层上的属性
719 */
720 popupProps: PropTypes.object,
721 /**
722 * 是否跟随滚动
723 */
724 followTrigger: PropTypes.bool,
725 /**
726 * 自定义弹层的内容
727 */
728 popupContent: PropTypes.node,
729 /**
730 * 添加到菜单上的属性
731 * @version 1.18
732 */
733 menuProps: PropTypes.object,
734 /**
735 * 是否使用本地过滤,在数据源为远程的时候需要关闭此项
736 */
737 filterLocal: PropTypes.bool,
738 /**
739 * 本地过滤方法,返回一个 Boolean 值确定是否保留
740 * @param {String} key 搜索关键字
741 * @param {Object} item 渲染节点的item
742 * @return {Boolean} 是否匹配
743 */
744 filter: PropTypes.func,
745 /**
746 * 默认高亮的 key,不要和 autoHighlightFirstItem 同时使用
747 */
748 defaultHighlightKey: PropTypes.string,
749 /**
750 * 高亮 key,不要和 autoHighlightFirstItem 同时使用,用于受控模式
751 */
752 highlightKey: PropTypes.string,
753 /**
754 * 键盘上下键切换菜单高亮选项的回调
755 */
756 onToggleHighlightItem: PropTypes.func,
757 /**
758 * 自动高亮第一个元素
759 */
760 autoHighlightFirstItem: PropTypes.bool,
761 /**
762 * 是否开启虚拟滚动模式
763 */
764 useVirtual: PropTypes.bool,
765 // 自定义类名
766 className: PropTypes.any,
767 children: PropTypes.any,
768 dataSource: PropTypes.array,
769 itemRender: PropTypes.func,
770 mode: PropTypes.string,
771 notFoundContent: PropTypes.node,
772 locale: PropTypes.object,
773 rtl: PropTypes.bool,
774 popupComponent: PropTypes.any,
775 /**
776 * 是否为预览态
777 */
778 isPreview: PropTypes.bool,
779 /**
780 * 预览态模式下渲染的内容
781 * @param {number} value 评分值
782 */
783 renderPreview: PropTypes.func,
784 showDataSourceChildren: PropTypes.bool
785}, _class.defaultProps = {
786 prefix: 'next-',
787 size: 'medium',
788 autoWidth: true,
789 onChange: noop,
790 onVisibleChange: noop,
791 onToggleHighlightItem: noop,
792 popupProps: {},
793 filterLocal: true,
794 filter: filter,
795 itemRender: function itemRender(item) {
796 return item.label || item.value;
797 },
798 locale: zhCN.Select,
799 autoHighlightFirstItem: true,
800 showDataSourceChildren: true,
801 defaultHighlightKey: null
802}, _temp);
803Base.displayName = 'Base';
804export { Base as default };
\No newline at end of file