/** * Popup */ import * as React from 'react'; import { Component } from 'react'; import noop from 'zent/es/utils/noop'; import Popover from '../popover'; import { I18nReceiver as Receiver, II18nLocaleSelect } from '../i18n'; import Search from './components/Search'; import Option from './components/Option'; import defer from 'zent/es/utils/defer'; import { addEventListener } from 'zent/es/utils/component/event-handler'; export interface IPopupProps { adjustPosition: () => void; cid: string; keyword: any; selectedItems: any[]; searchPlaceholder: string; emptyText: any; prefixCls: string; extraFilter: boolean; filter: (items: T[], keyword: string) => T[]; autoWidth: boolean; popover: any; onAsyncFilter: (keyword: string) => void; data: any[]; maxToShow?: number; ready?: boolean; onChange: (evt: React.ReactEventHandler, items: any[]) => void; } class Popup extends Component { static defaultProps = { adjustPosition: noop, cid: -1, keyword: '', selectedItems: [], emptyText: '', prefixCls: '', extraFilter: false, searchPlaceholder: '', }; focused: boolean; popup: HTMLDivElement | null = null; itemIds: string[]; currentIdUpdated: boolean; cancelEvent: () => void = null; constructor(props) { super(props); this.state = { data: props.data, // 默认选中第一项 currentId: props.data[0] ? props.data[0].cid : 0, keyword: props.keyword || '', style: {}, }; this.focused = false; } componentDidMount() { const { autoWidth, popover } = this.props; if (autoWidth) { this.setState({ style: { width: `${popover.getTriggerNode().clientWidth + 2}px`, }, }); } this.cancelEvent = addEventListener( this.popup, 'mousewheel', this.handleScroll ); } componentWillUnmount() { this.cancelEvent(); } handleScroll = evt => { evt.stopPropagation(); if ( (this.popup.scrollTop === 0 && evt.deltaY < 0) || (this.popup.scrollTop + this.popup.clientHeight === this.popup.scrollHeight && evt.deltaY > 0) ) { evt.preventDefault(); } }; // 等重构再删了吧,改不动 // eslint-disable-next-line react/no-deprecated componentWillReceiveProps(nextProps) { // 渲染时在 popover content ready 后延时触发 focus, 只触发一次 // NOTE: win7 360浏览器, 兼容性 bug 修复 if ( !this.focused && nextProps.ready && !nextProps.filter && !nextProps.onAsyncFilter ) { setTimeout(() => { this.popup && this.popup.focus(); }, 150); this.focused = true; } const keyword = nextProps.keyword; this.setState({ data: nextProps.data, // 默认选中第一项 currentId: nextProps.data[0] ? nextProps.data[0].cid : 0, }); // trigger中传入的keyword if ( this.props.extraFilter && keyword !== null && keyword !== this.state.keyword ) { this.searchFilterHandler(keyword); } } optionChangedHandler = (ev, cid) => { const { filter, data } = this.props; const { keyword } = this.state; this.setState({ keyword: '', }); this.props.popover.close(); this.props.onChange( ev, data.filter( item => (!keyword || !filter || filter(item, `${keyword}`)) && item.cid === cid )[0] ); }; searchFilterHandler = (keyword: string) => { const { onAsyncFilter, filter, adjustPosition } = this.props; // keyword = trim(keyword); 防止空格输入不进去 let { data, currentId } = this.state; const shouldFilter = !!keyword && !!filter; for (let i = 0; i < data.length; i++) { const item = data[i]; if ((shouldFilter && filter(item, keyword)) || !currentId) { currentId = item.cid; break; } } this.setState({ keyword, currentId, }); if (typeof onAsyncFilter === 'function') { onAsyncFilter(`${keyword}`); } else { // 同步关键词过滤后更新 Popup 位置 defer(() => { adjustPosition(); }); } }; keydownHandler = ev => { const code = ev.key; const itemIds = this.itemIds; let { currentId, keyword } = this.state; const index = itemIds.indexOf(currentId); const popupHeight = this.popup.clientHeight; const scrollHeight = this.popup.scrollHeight; const currentNode = this.popup.getElementsByClassName( 'current' )[0] as HTMLElement; switch (code) { case 'Escape': this.props.popover.close(); break; case 'ArrowDown': ev.preventDefault(); if (this.itemIds[index + 1]) { currentId = this.itemIds[index + 1]; this.currentIdUpdated = true; if ( currentNode && currentNode.offsetTop + 28 >= this.popup.scrollTop + popupHeight ) { this.popup.scrollTop = currentNode.offsetTop + 28 * 2 - popupHeight; } } else { currentId = this.itemIds[0]; this.popup.scrollTop = 0; } break; case 'ArrowUp': ev.preventDefault(); if (index > 0) { currentId = this.itemIds[index - 1]; this.currentIdUpdated = true; if (currentNode && currentNode.offsetTop <= this.popup.scrollTop) { this.popup.scrollTop = currentNode.offsetTop - 28; } } else { currentId = this.itemIds[this.itemIds.length - 1]; this.popup.scrollTop = scrollHeight; } break; case 'Enter': ev.preventDefault(); this.optionChangedHandler(keyword, currentId); this.currentIdUpdated = false; break; default: break; } this.setState({ currentId, }); }; updateCurrentId(cid) { this.setState({ currentId: cid, }); } render() { const { cid, selectedItems, emptyText, prefixCls, extraFilter, searchPlaceholder, filter, onAsyncFilter, maxToShow, autoWidth, ready, } = this.props; const { keyword, data, currentId } = this.state; let filterData = data.filter(item => { return !keyword || !filter || filter(item, `${keyword}`); }); const showEmpty = data.length === 0 || filterData.length === 0; this.itemIds = filterData.map(item => item.cid); if (maxToShow && !extraFilter && filter) { filterData = filterData.slice(0, maxToShow); } return (
(this.popup = popup)} className={`${prefixCls}-popup`} onKeyDown={this.keydownHandler} tabIndex={0} style={autoWidth ? this.state.style : null} onFocus={event => { event.preventDefault(); }} > {!extraFilter && (filter || onAsyncFilter) ? ( ) : ( '' )} {filterData.map((item, index) => { const currentCls = item.cid === currentId ? 'current' : ''; const activeCls = selectedItems.filter(o => o.cid === item.cid).length > 0 || item.cid === cid ? 'active' : ''; return (
); } } export default Popover.withPopover(Popup) as React.ComponentType;