import { Children, Component, ReactNode } from 'react'; import cx from 'classnames'; import AlertItem from './components/AlertItem'; import { AlertItem as AlertItemPub } from './AlertItem'; import { AlertTypes } from './types'; import { PartialRequired } from '../utils/types'; import kindOf from '../utils/kindOf'; import omit from '../utils/omit'; /** * 为满足动画的无缝衔接 * 在原子节点后增加第一个子节点 */ function cloneChildren(children: ReactNode): ReactNode[] { const length = Children.count(children); const clonedChildren: ReactNode[] = new Array(length); Children.forEach(children, (child, index) => { clonedChildren[index] = child; if (index === 0) { clonedChildren[length] = child; } }); return length > 1 ? clonedChildren : [children]; } /** * 根据props的children,获取有效的渲染节点 * @param children props的子节点 */ function getRenderChildrenFromProps(children: ReactNode) { const childArray = Children.toArray(children); // children类型校验 const items = childArray.reduce( (alertItemArray, child: React.ReactElement) => { const type = child.type; if (kindOf(type, AlertItemPub)) { alertItemArray.push(child); } return alertItemArray; }, [] ); const renderItems = cloneChildren(items); return { items, preChildren: children, renderItems }; } export interface IScrollAlertProps extends Omit, 'title'> { type?: AlertTypes; outline?: boolean; loading?: boolean; scrollInterval?: number; onClose?: () => void; closed?: boolean; bordered?: boolean; } interface IState { items: ReactNode[]; renderItems: ReactNode[]; preChildren: ReactNode; transitionDuration: number; containerHeight: number; activeIndex: number; } type IScrollAlertInnerProps = PartialRequired< IScrollAlertProps, 'loading' | 'scrollInterval' | 'onClose' | 'closed' >; const OmitDivAttr = ['loading', 'scrollInterval', 'onClose', 'closed'] as const; const DefaultState: IState = { items: [], renderItems: [], preChildren: null, transitionDuration: 0, containerHeight: 0, activeIndex: 0, }; export class ScrollAlert extends Component { static defaultProps = { type: 'info', loading: false, scrollInterval: 5000, bordered: false, }; constructor(props) { super(props); this.state = { ...DefaultState, ...getRenderChildrenFromProps(props.children), }; } // timeout事件id timeoutId: any; // 第一个子节点的高度 firstChildHeight = 0; componentDidMount() { this.setState( { containerHeight: this.firstChildHeight }, this.scrollHandler ); } componentWillUnmount() { this.clearTimer(); } /** * 节点滚动事件 */ scrollHandler = () => { const { scrollInterval } = this.props; this.timeoutId = setTimeout(() => { const { renderItems, activeIndex } = this.state; const length = renderItems.length; // 空节点、一个节点均不产生动画 if (length <= 1) return; const index = activeIndex + 1; this.setState({ transitionDuration: 600, activeIndex: index, }); // 滚动到最后一个节点时,重置为初始位置 if (index === length - 1) { setTimeout(this.resetChildren, 600); } this.scrollHandler(); }, scrollInterval); }; // 鼠标移入,动画暂停 stopScroll = () => { this.clearTimer(); }; // 鼠标移出,动画继续 continueScroll = () => { this.scrollHandler(); }; /** * 重置节点为0 */ resetChildren = () => { this.setState({ transitionDuration: 0, activeIndex: 0, }); }; /** * 清除timeout */ clearTimer = () => { if (this.timeoutId) { clearTimeout(this.timeoutId); this.timeoutId = null; } }; /** * 关闭回调函数 */ private onCloseItemHandler = index => { const { onClose } = this.props; const { items } = this.state; // 点击虚拟节点时,实际索引为0 if (index === items.length) { index = 0; this.resetChildren(); } // 删除items元素 const afterDeleteItems = items.filter((_, i) => index !== i); // 删除所有节点时,清除timeout并触发close回调 if (afterDeleteItems.length === 0) { onClose?.(); } // items只有一个元素时, 删除最后一项 else if ( afterDeleteItems.length === 1 || index === afterDeleteItems.length ) { this.resetChildren(); } this.setState({ items: afterDeleteItems, renderItems: cloneChildren(afterDeleteItems), }); }; onFirstChildRef = itemInstance => { this.firstChildHeight = itemInstance?.offsetHeight || 0; }; // 实际dom中需要渲染的子节点 get renderItem() { const { outline, children, onClose, className, ...restItemProps } = this.props; const { renderItems, activeIndex } = this.state; const length = renderItems.length; return Children.map(renderItems, (item: React.ReactElement, index) => { const props = Object.assign({}, restItemProps, { ...item.props }); return ( this.onCloseItemHandler(index)} ref={!index ? this.onFirstChildRef : undefined} /> ); }); } render() { if (this.props.closed) { return null; } const { className, outline, type, bordered, ...restDivAttrs } = omit( this.props as IScrollAlertInnerProps, OmitDivAttr ); const { transitionDuration, containerHeight, activeIndex } = this.state; const renderItem = this.renderItem; const scrollCls = cx( 'zent-alert-scroll', `zent-alert-style-${type}`, className, { ['zent-alert-scroll-outline']: outline, ['zent-alert-scroll--borderless']: !bordered, } ); return renderItem.length > 0 ? (
{renderItem}
) : null; } } export default ScrollAlert;