dropdown-content.tsx 4.6 KB
import React, {
  memo,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import {QxBaseIcon} from '@qx/common';
import { useScroll } from 'ahooks';
import { Empty, Input, Menu, Spin } from 'antd';
import cls from 'classnames';
import { debounce } from 'lodash-es';
import './styles.less';
const prefix = 'qx-input-select-dropdown';

const listItemHeight = 40;

const defaultProps = {
  fieldNames: {
    label: 'name',
    value: 'code',
  },
};

const DropdownContent = React.forwardRef<any, DropdownContentProps>(
  (props, forwardRef) => {
    const {
      options = [],
      fieldNames = defaultProps.fieldNames,
      onPopupScroll,
      onChange,
      className,
      style,
      renderBottom,
      showSearch = false,
      onSearch: parentSearch,
      placeholder,
      listHeight = 192,
      loading,
    } = props;

    const [filteredOpts, setFilteredOpts] = useState<any[]>(options || []);
    const ref = useRef(null);
    const position = useScroll(ref);
    const isEmpty = !filteredOpts.length;
    const fieldLabel = fieldNames.label || 'name';
    const fieldValue = fieldNames.value || 'code';

    const computedOptions = useMemo(
      () =>
        options?.map((opt: any) => ({
          label: opt[fieldLabel],
          key: opt[fieldValue],
          ...(opt['extract']? {extract: opt['extract']}: {})
        })),
      [options],
    );

    const handleSearch = useCallback(
      debounce((e) => {
        const val = e.target?.value;
        const newOpts = val
          ? computedOptions.filter((opt) => {
              return opt.label?.includes(val);
            })
          : computedOptions;
        setFilteredOpts(newOpts);
      }, 800),
      [computedOptions],
    );

    const onSearch = (e: any) => {
      e.persist();
      if (parentSearch) {
        parentSearch?.(e.target?.value);
      } else {
        handleSearch(e);
      }
    };

    const findSelected = (key: string) =>
      filteredOpts?.find((opt) => opt.key === key);

    const handleChange = ({ key }: any) => {
      const result = findSelected(key);
      onChange?.({
        ...result,
        [fieldLabel]: result?.label,
        [fieldValue]: result?.key,
      });
    };

    useEffect(() => {
      const length = options?.length;

      if (length < 6) return;

      if (position) {
        const _listHeight = length * listItemHeight;

        const isBottom = _listHeight - _listHeight <= position.top;

        onPopupScroll?.(position, isBottom);
      }
    }, [position, options]);

    useEffect(() => {
      if (Array.isArray(computedOptions)) {
        setFilteredOpts(computedOptions);
      }
    }, [computedOptions]);

    return (
      <div ref={forwardRef} className={cls(prefix, className)} style={style}>
        {showSearch ? (
          <Input
            placeholder={placeholder || '输入名称搜索'}
            prefix={<QxBaseIcon type={'icon-app-search-line'} />}
            className={`${prefix}__input`}
            onChange={onSearch}
            bordered={false}
            size={'small'}
          />
        ) : null}

        {loading ? (
          <Spin spinning={loading} />
        ) : (
          <div
            ref={ref}
            className={`${prefix}__list`}
            style={{ maxHeight: listHeight }}
          >
            {isEmpty ? (
              <Empty
                className={`${prefix}__list-empty`}
                image={Empty.PRESENTED_IMAGE_SIMPLE}
              />
            ) : (
              <Menu
                className={cls(`${prefix}__list-content`, {
                  [`${prefix}__list-content--has-bottom`]: !!renderBottom,
                })}
                items={filteredOpts}
                onClick={handleChange}
              />
            )}
          </div>
        )}
        <div className={`${prefix}__list-content-bottom`}>{renderBottom}</div>
      </div>
    );
  },
);

interface FieldNames {
  label: string;
  value: string;
  options?: any;
}

export interface DropdownContentOptions {
  name?: string | React.ReactNode;
  code?: string | number;
  appId?: string;
  formId?: string | number;
  isTree?: boolean;
}

export interface DropdownContentProps {
  placeholder?: string;
  options?: DropdownContentOptions[];
  fieldNames?: FieldNames;
  onPopupScroll?: (
    options: { left: number; top: number },
    isBottom: boolean,
  ) => void;
  onChange?: (val: any) => void;
  onOpenOther?: () => void;
  showSearch?: boolean;
  onSearch?: (val: string) => void;
  renderBottom?: React.ReactNode;
  listHeight?: number;
  loading?: boolean;
  className?: string;
  style?: React.CSSProperties;
}

export default memo(DropdownContent);