欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

比ant更豐富Modal組件功能實現(xiàn)示例詳解

 更新時間:2022年11月29日 09:37:35   作者:孟祥_成都  
這篇文章主要為大家介紹了比ant更豐富Modal組件功能實現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

有哪些比ant更豐富的功能

普通的modal組件如下:

我們寫的modal額外支持,后面沒有蒙版,并且Modal框能夠拖拽

還支持渲染在文檔流里,上面的都是fixed布局,我們這個正常渲染到文檔下面:

render部分

    <RenderDialog
      {...restState}
      visible={visible}
      prefixCls={prefixCls}
      header={renderHeader}
      attach={attach}
      closeBtn={renderCloseIcon()}
      classPrefix={classPrefix}
      onClose={onClose}
      onConfirm={onConfirm}
      footer={footer === true ? defaultFooter() : footer}
      ref={dialogDom}
    />

大家記住這個RenderDialog,接下來都是上面?zhèn)鲄⒌慕忉專?/p>

resetState: 是對象,一堆屬性的集合,哪些屬性呢,我們往下看

// 其實默認參數(shù)寫到這里并不科學,因為react有個靜態(tài)屬性defaultProps屬性支持合并props
  const [state, setState] = useSetState<DialogProps>({
    width: 520, // 默認寬度是520
    visible: false, // 默認visible是false
    zIndex: 2500, // 默認zIndex 2500
    placement: 'center', // 默認渲染到屏幕中間
    mode: 'modal', // 默認的模式是modal是ant那種渲染結果,其他模式我們下面談
    showOverlay: true, // 是否展示透明黑色蒙版
    destroyOnClose: false, // 關閉彈窗的時候是否銷毀里面的內容
    draggable: false, // 是否能拖拽modal
    preventScrollThrough: true, // 防止?jié)L動穿透
    ...props,
  });

restState在下面,除了state上某些屬性。

 const {
   visible, // 控制對話框是否顯示
   attach, // 對話框掛載的節(jié)點,默認掛在組件本身的位置。數(shù)據(jù)類型為 String 時,會被當作選擇器處理,進行節(jié)點查詢。示例:'body' 或 () => document.body
   closeBtn, // 關閉按鈕,可以自定義。值為 true 顯示默認關閉按鈕,值為 false 不顯示關閉按鈕。值類型為 string 則直接顯示值,// 底部操作欄,默認會有“確認”和“取消”兩個按鈕。值為 true 顯示默認操作按鈕,值為 false 不顯示任何內容,值類型為 Function 表示自定義底部內容
   footer = true, // 如果“取消”按鈕存在,則點擊“取消”按鈕時觸發(fā),同時觸發(fā)關閉事件
   onCancel = noop, // 如果“確認”按鈕存在,則點擊“確認”按鈕時觸發(fā),或者鍵盤按下回車鍵時觸發(fā)
   onConfirm = noop, // 如果“確認”按鈕存在,則點擊“確認”按鈕時觸發(fā),或者鍵盤按下回車鍵時觸發(fā)
   cancelBtn = cancelText, // 取消按鈕,可自定義。值為 null 則不顯示取消按鈕。值類型為字符串,則表示自定義按鈕文本,值類型為 Object 則表示透傳 Button 組件屬性。
   confirmBtn = confirmText, // 確認按鈕。值為 null 則不顯示確認按鈕。值類型為字符串,則表示自定義按鈕文本,值類型為 Object 則表示透傳 Button 組件屬性。
   onClose = noop, // 關閉事件,點擊取消按鈕、點擊關閉按鈕、點擊蒙層、按下 ESC 等場景下觸發(fā)
   ...restState
 } = state;

說了這么多,我們接著看RenderDialog組件上傳入的屬性。

prefixCls不講了,是css屬性前綴,一個字符串,接著看header屬性被包裝為renderHeader

const renderHeader = useMemo(() => {
    if (!state.header) return null;
    const iconMap = {
      info: <InfoCircleFilledIcon className={`${classPrefix}-is-info`} />,
      warning: <InfoCircleFilledIcon className={`${classPrefix}-is-warning`} />,
      error: <InfoCircleFilledIcon className={`${classPrefix}-is-error`} />,
      success: <CheckCircleFilledIcon className={`${classPrefix}-is-success`} />,
    };
    return (
      <div className={`${prefixCls}__header-content`}>
        {iconMap[state.theme]}
        {state.header}
      </div>
    );
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [state.header, state.theme, prefixCls, classPrefix]);

其實就是在header的文字前面多了一個icon,比如成功的彈窗如下:

接著看closeBtn屬性

  const renderCloseIcon = () => {
    if (closeBtn === false) return null;
    if (closeBtn === true) return <CloseIcon style={{ verticalAlign: 'unset' }} />;
    return closeBtn || <CloseIcon style={{ verticalAlign: 'unset' }} />;
  };

這個是右上角關閉按鈕的Icon,很簡單,如果是false,什么都不許安然,如果是undefined或者true渲染這個icon。

好了,我們把整個代碼放到下面,有代碼注釋,沒寫注釋的是上面咋們已經講過的內容,接著就要進入RenderDialog這個組件內部了。

import 的部分省略了
// 渲染 footer的button方法
const renderDialogButton = (btn: TdDialogProps['cancelBtn'], defaultProps: ButtonProps) => {
  let result = null;
  if (isString(btn)) {
    result = <Button {...defaultProps}>{btn}</Button>;
  }  else if (isFunction(btn)) {
    result = btn();
  }
  return result;
};
const Dialog = forwardRef((props: DialogProps, ref: React.Ref<DialogInstance>) => {
  // 這部分忽略就好,用來獲取全局配置的css前綴字符串
  const { classPrefix } = useConfig();
  // 這個也忽略,獲取icon組件的
  const { CloseIcon, InfoCircleFilledIcon, CheckCircleFilledIcon } = useGlobalIcon({
    CloseIcon: TdCloseIcon,
    InfoCircleFilledIcon: TdInfoCircleFilledIcon,
    CheckCircleFilledIcon: TdCheckCircleFilledIcon,
  });
  // 用來引用dialog彈框的dom
  const dialogDom = useRef<HTMLDivElement>();
  const [state, setState] = useSetState<DialogProps>({
    width: 520,
    visible: false,
    zIndex: 2500,
    placement: 'center',
    mode: 'modal',
    showOverlay: true,
    destroyOnClose: false,
    draggable: false,
    preventScrollThrough: true,
    ...props,
  });
 // 國際化有關的
  const [local, t] = useLocaleReceiver('dialog');
  const confirmText = t(local.confirm);
  const cancelText = t(local.cancel);
  const {
    visible,
    attach,
    closeBtn,
    footer = true,
    onCancel = noop,
    onConfirm = noop,
    cancelBtn = cancelText,
    confirmBtn = confirmText,
    onClose = noop,
    ...restState
  } = state;
  useEffect(() => {
     setState((prevState) => ({
        ...prevState,
        ...props,
      }));
  }, [props, setState, isPlugin]);
  const prefixCls = `${classPrefix}-dialog`;
  const renderCloseIcon = () => {
    if (closeBtn === false) return null;
    if (closeBtn === true) return <CloseIcon style={{ verticalAlign: 'unset' }} />;
    return closeBtn || <CloseIcon style={{ verticalAlign: 'unset' }} />;
  };
 // 這里把一些外部方法暴露給調用者,只需要傳入ref就可以獲取
  React.useImperativeHandle(ref, () => ({
    show() {
      setState({ visible: true });
    },
    hide() {
      setState({ visible: false });
    },
    destroy() {
      setState({ visible: false, destroyOnClose: true });
    },
    update(newOptions) {
      setState((prevState) => ({
        ...prevState,
        ...(newOptions as DialogProps),
      }));
    },
  }));
  const renderHeader = useMemo(() => {
    if (!state.header) return null;
    const iconMap = {
      info: <InfoCircleFilledIcon className={`${classPrefix}-is-info`} />,
      warning: <InfoCircleFilledIcon className={`${classPrefix}-is-warning`} />,
      error: <InfoCircleFilledIcon className={`${classPrefix}-is-error`} />,
      success: <CheckCircleFilledIcon className={`${classPrefix}-is-success`} />,
    };
    return (
      <div className={`${prefixCls}__header-content`}>
        {iconMap[state.theme]}
        {state.header}
      </div>
    );
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [state.header, state.theme, prefixCls, classPrefix]);
// 渲染footer的時候,點擊取消按鈕會用到
  const handleCancel = (e: React.MouseEvent<HTMLButtonElement>) => {
    onCancel({ e });
    onClose({ e, trigger: 'cancel' });
  };
// 渲染footer的時候,點擊確認按鈕會用到
  const handleConfirm = (e: React.MouseEvent<HTMLButtonElement>) => {
    onConfirm({ e });
  };
  const defaultFooter = () => {
    const renderCancelBtn = renderDialogButton(cancelBtn, { variant: 'outline' });
    const renderConfirmBtn = renderDialogButton(confirmBtn, { theme: 'primary' });
    return (
      <>
        {renderCancelBtn &&
          React.cloneElement(renderCancelBtn, {
            onClick: handleCancel,
            ...renderCancelBtn.props,
          })}
        {renderConfirmBtn &&
          React.cloneElement(renderConfirmBtn, {
            onClick: handleConfirm,
            ...renderConfirmBtn.props,
          })}
      </>
    );
  };
  return (
    <RenderDialog
      {...restState}
      visible={visible}
      prefixCls={prefixCls}
      header={renderHeader}
      attach={attach}
      closeBtn={renderCloseIcon()}
      classPrefix={classPrefix}
      onClose={onClose}
      onConfirm={onConfirm}
      footer={footer === true ? defaultFooter() : footer}
      ref={dialogDom}
    />
  );
});
Dialog.displayName = 'Dialog';
Dialog.defaultProps = dialogDefaultProps;
export default Dialog;

接著,我們要渲染的部分其實很簡單,包括

  • 背后的黑色蒙層
  • 彈框
    • 彈框的標題
    • 彈框的內容區(qū)域
    • 彈框的footer
  • 還需要彈框動畫,比如zoom或者fade

渲染黑色蒙層

代碼如下,很簡單

  const renderMask = () => {
    let maskElement;
    if (showOverlay) {
      maskElement = (
        <CSSTransition
          in={visible}
          appear
          timeout={transitionTime}
          classNames={`${prefixCls}-fade`}
          mountOnEnter
          unmountOnExit
          nodeRef={maskRef}
        >
          <div ref={maskRef} className={`${prefixCls}__mask`} />
        </CSSTransition>
      );
    }
    return maskElement;
  };

首先介紹一下CSSTransition,這是react-transition-group動畫庫的一個組件,用來幫助我們實現(xiàn)css動畫的。 其中一些屬性說明如下:

  • in: ture就是開始動畫,false就是停止動畫
  • appear:boolean,為 false 時當 CSSTransition 控件加載完畢后不執(zhí)行動畫,為 true 時控件加載完畢則立即執(zhí)行動畫。如果要組件初次渲染就有動畫,則需要設成 true。
  • timeout 動畫時間
  • classNames:動畫的類名,比如classNames:'demo',會自動在進入動畫的時候幫你把類名改為 demo-enter-active, demo-enter-done, 在退出動畫同樣會有類名的改變。
  • mountOnEnter:一進來的時候不顯示dom元素
  • unmountOnExit:boolean,為 true 時組件將移除處于隱藏狀態(tài)的元素,為 false 時組件保持動畫結束時的狀態(tài)而不移除元素。一般要設成 true。
  • nodeRef,獲取蒙層的ref

蒙層主要靠css實現(xiàn),我們看下css

  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 1;
  background: var(--td-mask-active);
  pointer-events: auto;

渲染彈框主體

也非常簡單啊,我們把注釋寫在下面的代碼里了,其中有一個需要小小注意的功能就是拖拽功能

// 渲染Dialog主體
  const renderDialog = () => {
    const dest: any = {};
    // 把width變?yōu)橛衟x結尾的字符串
    if (props.width !== undefined) {
      dest.width = GetCSSValue(props.width);
    }
    // normal 場景下,需要設置 zindex 為auto 避免出現(xiàn)多個 dialog,normal 出現(xiàn)在最上層
    if (props.mode === 'normal') {
      dest.zIndex = 'auto';
    }
    // 獲取footer
    const footer = props.footer ? <div className={`${prefixCls}__footer`}>{props.footer}</div> : null;
    // 獲取header
    const { header } = props;
    // 獲取Dialog body
    const body = <div className={`${prefixCls}__body`}>{props.body || props.children}</div>;
    // 關閉按鈕,可以自定義。值為 true 顯示默認關閉按鈕,值為 false 不顯示關閉按鈕。值類型為 string 則直接顯示值,如:“關閉”。
    const closer = closeBtn && (
      <span onClick={handleCloseBtnClick} className={`${prefixCls}__close`}>
        {closeBtn}
      </span>
    );
    const validWindow = typeof window === 'object';
    // 獲取屏幕高度
    const screenHeight = validWindow ? window.innerHeight || document.documentElement.clientHeight : undefined;
    // 獲取屏幕寬度
    const screenWidth = validWindow ? window.innerWidth || document.documentElement.clientWidth : undefined;
    // 設置style
    const style = { ...dest, ...props.style };
    let dialogOffset = { x: 0, y: 0 };
    // 拖拽代碼實現(xiàn)部分
    const onDialogMove = (e: MouseEvent) => {
      // offsetWidth是指元素的寬 + padding + border的總和
      const { style, offsetWidth, offsetHeight } = dialog.current;
      // diffX是指彈框部分距離body左邊部分
      let diffX = e.clientX - dialogOffset.x;
      let diffY = e.clientY - dialogOffset.y;
      // 拖拽上左邊界限制
      if (diffX < 0) diffX = 0;
      if (diffY < 0) diffY = 0;
      // 右邊的限制
      if (screenWidth - offsetWidth - diffX < 0) diffX = screenWidth - offsetWidth;
      // 下邊的限制
      if (screenHeight - offsetHeight - diffY < 0) diffY = screenHeight - offsetHeight;
      style.position = 'absolute';
      style.left = `${diffX}px`;
      style.top = `${diffY}px`;
    };
    const onDialogMoveEnd = () => {
      // 恢復指針樣式為默認,并且注銷mousemove, mouseup事件
      dialog.current.style.cursor = 'default';
      document.removeEventListener('mousemove', onDialogMove);
      document.removeEventListener('mouseup', onDialogMoveEnd);
    };
    // 拖拽開始,對應mouseDown事件
    const onDialogMoveStart = (e: React.MouseEvent<HTMLDivElement>) => {
      contentClickRef.current = true;
      // 阻止事件冒泡, mode === 'modeless才能拖拽
      if (canDraggable && e.currentTarget === e.target) {
        const { offsetLeft, offsetTop, offsetHeight, offsetWidth } = dialog.current;
        // 如果彈出框超出屏幕范圍 不能進行拖拽
        if (offsetWidth > screenWidth || offsetHeight > screenHeight) return;
        // 拖拽樣式設置為move
        dialog.current.style.cursor = 'move';
        // 計算鼠標 e.clientX是鼠標在屏幕的坐標,offsetLeft是Dialog主體跟body的距離
        // 所以e.clientX - offsetLeft就是鼠標在是Dialog主體上的橫坐標
        const diffX = e.clientX - offsetLeft;
        const diffY = e.clientY - offsetTop;
        dialogOffset = {
          x: diffX,
          y: diffY,
        };
        // 此時把mousemove和mouseup事件也綁定一下,其實不建議綁定在這里直接操作dom
        document.addEventListener('mousemove', onDialogMove);
        document.addEventListener('mouseup', onDialogMoveEnd);
      }
    };
    // 頂部定位實現(xiàn)
    const positionStyle: any = {};
    if (props.top) {
      const topValue = GetCSSValue(props.top);
      positionStyle.paddingTop = topValue;
    }
    // 此處獲取定位方式 top 優(yōu)先級較高 存在時 默認使用 top 定位
    const positionClass = classnames(
      `${prefixCls}__position`,
      { [`${prefixCls}--top`]: !!props.top },
      `${props.placement && !props.top ? `${prefixCls}--${props.placement}` : ''}`,
    );
    // 然后就是用css去渲染header body和footer
    const dialogElement = (
      <div className={isNormal ? '' : `${prefixCls}__wrap`}>
        <div className={isNormal ? '' : positionClass} style={positionStyle} onClick={onMaskClick} ref={dialogPosition}>
          <div
            ref={dialog}
            style={style}
            className={classnames(`${prefixCls}`, `${prefixCls}--default`)}
            onMouseDown={onDialogMoveStart}
          >
            <div className={classnames(`${prefixCls}__header`)}>
              {header}
              {closer}
            </div>
            {body}
            {footer}
          </div>
        </div>
      </div>
    );
    return (
      <CSSTransition
        in={props.visible}
        appear
        mountOnEnter
        unmountOnExit={destroyOnClose}
        timeout={transitionTime}
        classNames={`${prefixCls}-zoom`}
        onEntered={props.onOpened}
        onExited={onAnimateLeave}
        nodeRef={dialog}
      >
        {dialogElement}
      </CSSTransition>
    );
  };

我們這里貼一下css部分:

header:

.t-dialog__header {
    color: var(--td-text-color-primary);
    font: var(--td-font-title-medium);
    font-weight: 600;
    display: flex;
    align-items: flex-start;
    word-break: break-word;
}

這里注意下:word-wrap:break-word

它會把整個單詞看成一個整體,如果該行末端寬度不夠顯示整個單詞,它會自動把整個單詞放到下一行,而不會把單詞截斷掉的。

body

.t-dialog__body {
  padding: 16px 0;
  color: var(--td-text-color-secondary);
  font: var(--td-font-body-medium);
  overflow: auto;
  word-break: break-word;
}

footer

  width: 100%;
  text-align: right;
  padding: 16px 0 0 0;

好了,我們結合一下彈框和蒙層,看下render函數(shù)

const render = () => {
    // 。。。省略css部分
    // 如果不是 modal 模式 默認沒有 mask 也就沒有相關點擊 mask 事件
    const dialog = (
      <div ref={wrap} className={wrapClass} style={wrapStyle} onKeyDown={handleKeyDown} tabIndex={0}>
        {mode === 'modal' && renderMask()}
        {dialogBody} // 這里就是我們上面講的renderDialog
      </div>
    );
    return dialog;
  };

設置body overflow:hiiden

為啥要設置body overflow:hiiden這個屬性呢,你打開modal彈窗的時候,如果此時body還有滾動條,那么你滾動鼠標滾輪還可以向下滑動,但是一般情況下,我們打開彈框,是希望用戶目標鎖定在當前交互,此時最好不要允許用戶滾動界面。

當然你也可以允許用戶滾動,我們用一個preventScrollThrough參數(shù)控制。

先記住當前body的css樣式,以及body的overflow的值,代碼如下

  useLayoutEffect(() => {
    bodyOverflow.current = document.body.style.overflow;
    bodyCssTextRef.current = document.body.style.cssText;
  }, []);
 const isModal = mode === 'modal';
useLayoutEffect(() => {
      // 只有modal數(shù)量小于1的時候才重置樣式,因為可能出現(xiàn)多個彈框,那么關閉一個彈框就出現(xiàn)滾動條明顯不對
      if (isModal) {
      const openDialogDom = document.querySelectorAll(`${prefixCls}__mode`);
      if (openDialogDom.length < 1) {
        document.body.style.cssText = bodyCssTextRef.current;
      }
    // 組件銷毀后重置 body 樣式
    return () => {
      if (isModal) {
        // 此處只能查詢 mode 模式的 dialog 個數(shù) 因為 modeless 會點擊透傳 normal 是正常文檔流
        const openDialogDom = document.querySelectorAll(`${prefixCls}__mode`);
        if (openDialogDom.length < 1) {
          document.body.style.cssText = bodyCssTextRef.current;
          document.body.style.overflow = bodyOverflow.current;
        }
      } 
    };
  }, [preventScrollThrough, attach, visible, mode, isModal, showInAttachedElement, prefixCls]);

上面的代碼還有一個問題,就是我們需要preventScrollThrough這個參數(shù)去控制是否可以body滾動頁面,這個也是算比ant更豐富的功能。

const isModal = mode === 'modal';
 useLayoutEffect(() => {
    // 處于顯示態(tài)
    if (visible) {
      // isModal表示是否是普通彈框,就是帶黑色蒙層的
      // bodyOverflow.current 引用的是body的overflow屬性
      // preventScrollThrough是代表是否可以滾動body
      // !showInAttachedElement表示不掛載到其他dom上
      if (isModal && bodyOverflow.current !== 'hidden' && preventScrollThrough && !showInAttachedElement) {
        // 求出滾動條的寬度
        const scrollWidth = window.innerWidth - document.body.offsetWidth;
        // 減少回流
        if (bodyCssTextRef.current === '') {
          let bodyCssText = 'overflow: hidden;';
          if (scrollWidth > 0) {
            bodyCssText += `position: relative;width: calc(100% - ${scrollWidth}px);`;
          }
          document.body.style.cssText = bodyCssText;
        } else {
          if (scrollWidth > 0) {
            document.body.style.width = `calc(100% - ${scrollWidth}px)`;
            document.body.style.position = 'relative';
          }
          document.body.style.overflow = 'hidden';
        }
      }
      // 剛進頁面就focus到彈框組件上
      if (wrap.current) {
        wrap.current.focus();
      }
    } else if (isModal) {
      const openDialogDom = document.querySelectorAll(`${prefixCls}__mode`);
      if (openDialogDom.length < 1) {
        document.body.style.cssText = bodyCssTextRef.current;
      }
    }
    // 組件銷毀后重置 body 樣式
    return () => {
      if (isModal) {
        // 此處只能查詢 mode 模式的 dialog 個數(shù) 因為 modeless 會點擊透傳 normal 是正常文檔流
        const openDialogDom = document.querySelectorAll(`${prefixCls}__mode`);
        if (openDialogDom.length < 1) {
          document.body.style.cssText = bodyCssTextRef.current;
          document.body.style.overflow = bodyOverflow.current;
        }
      } else {
        document.body.style.cssText = bodyCssTextRef.current;
        document.body.style.overflow = bodyOverflow.current;
      }
    };
  }, [preventScrollThrough, attach, visible, mode, isModal, showInAttachedElement, prefixCls]);

其實還有一個邏輯,是把彈窗渲染到任意dom里,需要一個Portal組件,我們這里就不說了,后續(xù)將Popup或者叫trigger組件的時候我們講吧。一篇文檔內容太多不好消化。

好了,主邏輯已經寫完了,很簡單吧!

接下來看下完整代碼,沒有注釋的部分是上面已經講過的

//省去了import
// 把css的數(shù)字轉為有px結尾的字符串,,這里其實應該寫到一個utils文件夾里,不應該跟主代碼混在一起
function GetCSSValue(v: string | number) {
  return Number.isNaN(Number(v)) ? v : `${Number(v)}px`;
}
// 動畫執(zhí)行時間,這里其實應該寫到一個constants文件里,不應該跟主代碼混在一起
const transitionTime = 300;
const RenderDialog = forwardRef((props: RenderDialogProps, ref: React.Ref<HTMLDivElement>) => {
  // 這里不用看,跟國際化有關
  const [local] = useLocaleReceiver('dialog');
  const {
    prefixCls, 
    attach, // 對話框掛載的節(jié)點,默認掛在組件本身的位置。數(shù)據(jù)類型為 String 時,會被當作選擇器處理,進行節(jié)點查詢。示例:'body' 或 () => document.body
    visible, // 控制對話框是否顯示
    mode, // 對話框類型,有三種:模態(tài)對話框、非模態(tài)對話框和普通對話框。彈出「模態(tài)對話框」時,只能操作對話框里面的內容,不能操作其他內容。彈出「非模態(tài)對話框」時,則可以操作頁面內所有內容。「普通對話框」是指沒有脫離文檔流的對話框,可以在這個基礎上開發(fā)更多的插件
    zIndex, // 對話框層級,Web 側樣式默認為 2500,移動端和小程序樣式默認為 1500
    showOverlay, // 是否顯示遮罩層
    onEscKeydown = noop,// 按下 ESC 時觸發(fā)事件
    onClosed = noop, // 對話框消失動畫效果結束后觸發(fā)
    onClose = noop, // 關閉事件,點擊取消按鈕、點擊關閉按鈕、點擊蒙層、按下 ESC 等場景下觸發(fā)
    onCloseBtnClick = noop, // 點擊右上角關閉按鈕時觸發(fā)
    onOverlayClick = noop, // 如果蒙層存在,點擊蒙層時觸發(fā)
    onConfirm = noop, // 如果“確認”按鈕存在,則點擊“確認”按鈕時觸發(fā),或者鍵盤按下回車鍵時觸發(fā)
    preventScrollThrough, // 防止?jié)L動穿透
    closeBtn, // 關閉按鈕,可以自定義。值為 true 顯示默認關閉按鈕,值為 false 不顯示關閉按鈕。值類型為 string 則直接顯示值,如:“關閉”。值類型為 TNode,則表示呈現(xiàn)自定義按鈕示例
    closeOnEscKeydown, // 按下 ESC 時是否觸發(fā)對話框關閉事件
    confirmOnEnter, // 是否在按下回車鍵時,觸發(fā)確認事件
    closeOnOverlayClick, // 點擊蒙層時是否觸發(fā)關閉事件
    destroyOnClose, // 是否在關閉彈框的時候銷毀子元素
    showInAttachedElement, // 僅在掛載元素中顯示抽屜,默認在瀏覽器可視區(qū)域顯示。父元素需要有定位屬性,如:position: relative
  } = props;
  const wrap = useRef<HTMLDivElement>(); // 掛載到包裹彈框的dom上,包裹了好幾層。。。
  const dialog = useRef<HTMLDivElement>(); // 引用彈窗dom
  const dialogPosition = useRef<HTMLDivElement>(); // 包裹彈窗,用于定位的dom引用
  const maskRef = useRef<HTMLDivElement>(); // 蒙層的dom引用
  const bodyOverflow = useRef<string>(); 
  const bodyCssTextRef = useRef<string>();
  const contentClickRef = useRef(false);
  const isModal = mode === 'modal';
  const isNormal = mode === 'normal';
  const canDraggable = props.draggable && mode === 'modeless';
  const dialogOpenClass = `${prefixCls}__${mode}`;
  useLayoutEffect(() => {
    bodyOverflow.current = document.body.style.overflow;
    bodyCssTextRef.current = document.body.style.cssText;
  }, []);
  useLayoutEffect(() => {
    if (visible) {
      if (isModal && bodyOverflow.current !== 'hidden' && preventScrollThrough && !showInAttachedElement) {
        const scrollWidth = window.innerWidth - document.body.offsetWidth;
        // 減少回流
        if (bodyCssTextRef.current === '') {
          let bodyCssText = 'overflow: hidden;';
          if (scrollWidth > 0) {
            bodyCssText += `position: relative;width: calc(100% - ${scrollWidth}px);`;
          }
          document.body.style.cssText = bodyCssText;
        } else {
          if (scrollWidth > 0) {
            document.body.style.width = `calc(100% - ${scrollWidth}px)`;
            document.body.style.position = 'relative';
          }
          document.body.style.overflow = 'hidden';
        }
      }
      if (wrap.current) {
        wrap.current.focus();
      }
    } else if (isModal) {
      const openDialogDom = document.querySelectorAll(`${prefixCls}__mode`);
      if (openDialogDom.length < 1) {
        document.body.style.cssText = bodyCssTextRef.current;
      }
    }
    // 組件銷毀后重置 body 樣式
    return () => {
      if (isModal) {
        // 此處只能查詢 mode 模式的 dialog 個數(shù) 因為 modeless 會點擊透傳 normal 是正常文檔流
        const openDialogDom = document.querySelectorAll(`${prefixCls}__mode`);
        if (openDialogDom.length < 1) {
          document.body.style.cssText = bodyCssTextRef.current;
          document.body.style.overflow = bodyOverflow.current;
        }
      } else {
        document.body.style.cssText = bodyCssTextRef.current;
        document.body.style.overflow = bodyOverflow.current;
      }
    };
  }, [preventScrollThrough, attach, visible, mode, isModal, showInAttachedElement, prefixCls]);
  const onAnimateLeave = () => {
    if (wrap.current) {
      wrap.current.style.display = 'none';
    }
    if (isModal && preventScrollThrough) {
      // 還原 body 的滾動條
      const openDialogDom = document.querySelectorAll(`${prefixCls}__mode`);
      if (isModal && openDialogDom.length < 1) {
        document.body.style.overflow = bodyOverflow.current;
      }
    }
    if (!isModal) {
      // 關閉彈窗 清空拖拽設置的相關 css
      const { style } = dialog.current;
      style.position = 'relative';
      style.left = 'unset';
      style.top = 'unset';
    }
    onClosed && onClosed();
  };
  const onMaskClick = (e: React.MouseEvent<HTMLDivElement>) => {
    if (showOverlay && (closeOnOverlayClick ?? local.closeOnOverlayClick)) {
      // 判斷點擊事件初次點擊是否為內容區(qū)域
      if (contentClickRef.current) {
        contentClickRef.current = false;
      } else if (e.target === dialogPosition.current) {
        onOverlayClick({ e });
        onClose({ e, trigger: 'overlay' });
      }
    }
  };
  const handleCloseBtnClick = (e: React.MouseEvent<HTMLDivElement>) => {
    onCloseBtnClick({ e });
    onClose({ e, trigger: 'close-btn' });
  };
  const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
    // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode
    if (e.key === 'Escape') {
      e.stopPropagation();
      onEscKeydown({ e });
      if (closeOnEscKeydown ?? local.closeOnEscKeydown) {
        onClose({ e, trigger: 'esc' });
      }
    } else if (e.key === 'Enter' || e.key === 'NumpadEnter') {
      // 回車鍵觸發(fā)點擊確認事件
      e.stopPropagation();
      if (confirmOnEnter) {
        onConfirm({ e });
      }
    }
  };
  // 渲染Dialog主體
  const renderDialog = () => {
    const dest: any = {};
    // 把width變?yōu)橛衟x結尾的字符串
    if (props.width !== undefined) {
      dest.width = GetCSSValue(props.width);
    }
    // normal 場景下,需要設置 zindex 為auto 避免出現(xiàn)多個 dialog,normal 出現(xiàn)在最上層
    if (props.mode === 'normal') {
      dest.zIndex = 'auto';
    }
    // 獲取footer
    const footer = props.footer ? <div className={`${prefixCls}__footer`}>{props.footer}</div> : null;
    // 獲取header
    const { header } = props;
    // 獲取Dialog body
    const body = <div className={`${prefixCls}__body`}>{props.body || props.children}</div>;
    // 關閉按鈕,可以自定義。值為 true 顯示默認關閉按鈕,值為 false 不顯示關閉按鈕。值類型為 string 則直接顯示值,如:“關閉”。
    const closer = closeBtn && (
      <span onClick={handleCloseBtnClick} className={`${prefixCls}__close`}>
        {closeBtn}
      </span>
    );
    const validWindow = typeof window === 'object';
    // 獲取屏幕高度
    const screenHeight = validWindow ? window.innerHeight || document.documentElement.clientHeight : undefined;
    // 獲取屏幕寬度
    const screenWidth = validWindow ? window.innerWidth || document.documentElement.clientWidth : undefined;
    // 設置style
    const style = { ...dest, ...props.style };
    let dialogOffset = { x: 0, y: 0 };
    // 拖拽代碼實現(xiàn)部分
    const onDialogMove = (e: MouseEvent) => {
      // offsetWidth是指元素的寬 + padding + border的總和
      const { style, offsetWidth, offsetHeight } = dialog.current;
      // diffX是指彈框部分距離body左邊部分
      let diffX = e.clientX - dialogOffset.x;
      let diffY = e.clientY - dialogOffset.y;
      // 拖拽上左邊界限制
      if (diffX < 0) diffX = 0;
      if (diffY < 0) diffY = 0;
      // 右邊的限制
      if (screenWidth - offsetWidth - diffX < 0) diffX = screenWidth - offsetWidth;
      // 下邊的限制
      if (screenHeight - offsetHeight - diffY < 0) diffY = screenHeight - offsetHeight;
      style.position = 'absolute';
      style.left = `${diffX}px`;
      style.top = `${diffY}px`;
    };
    const onDialogMoveEnd = () => {
      // 恢復指針樣式為默認,并且注銷mousemove, mouseup事件
      dialog.current.style.cursor = 'default';
      document.removeEventListener('mousemove', onDialogMove);
      document.removeEventListener('mouseup', onDialogMoveEnd);
    };
    // 拖拽開始,對應mouseDown事件
    const onDialogMoveStart = (e: React.MouseEvent<HTMLDivElement>) => {
      contentClickRef.current = true;
      // 阻止事件冒泡, mode === 'modeless才能拖拽
      if (canDraggable && e.currentTarget === e.target) {
        const { offsetLeft, offsetTop, offsetHeight, offsetWidth } = dialog.current;
        // 如果彈出框超出屏幕范圍 不能進行拖拽
        if (offsetWidth > screenWidth || offsetHeight > screenHeight) return;
        // 拖拽樣式設置為move
        dialog.current.style.cursor = 'move';
        // 計算鼠標 e.clientX是鼠標在屏幕的坐標,offsetLeft是Dialog主體跟body的距離
        // 所以e.clientX - offsetLeft就是鼠標在是Dialog主體上的橫坐標
        const diffX = e.clientX - offsetLeft;
        const diffY = e.clientY - offsetTop;
        dialogOffset = {
          x: diffX,
          y: diffY,
        };
        // 此時把mousemove和mouseup事件也綁定一下,其實不建議綁定在這里直接操作dom
        document.addEventListener('mousemove', onDialogMove);
        document.addEventListener('mouseup', onDialogMoveEnd);
      }
    };
    // 頂部定位實現(xiàn)
    const positionStyle: any = {};
    if (props.top) {
      const topValue = GetCSSValue(props.top);
      positionStyle.paddingTop = topValue;
    }
    // 此處獲取定位方式 top 優(yōu)先級較高 存在時 默認使用 top 定位
    const positionClass = classnames(
      `${prefixCls}__position`,
      { [`${prefixCls}--top`]: !!props.top },
      `${props.placement && !props.top ? `${prefixCls}--${props.placement}` : ''}`,
    );
    const dialogElement = (
      <div className={isNormal ? '' : `${prefixCls}__wrap`}>
        <div className={isNormal ? '' : positionClass} style={positionStyle} onClick={onMaskClick} ref={dialogPosition}>
          <div
            ref={dialog}
            style={style}
            className={classnames(`${prefixCls}`, `${prefixCls}--default`)}
            onMouseDown={onDialogMoveStart}
          >
            <div className={classnames(`${prefixCls}__header`)}>
              {header}
              {closer}
            </div>
            {body}
            {footer}
          </div>
        </div>
      </div>
    );
    return (
      <CSSTransition
        in={props.visible}
        appear
        mountOnEnter
        unmountOnExit={destroyOnClose}
        timeout={transitionTime}
        classNames={`${prefixCls}-zoom`}
        onEntered={props.onOpened}
        onExited={onAnimateLeave}
        nodeRef={dialog}
      >
        {dialogElement}
      </CSSTransition>
    );
  };
  const renderMask = () => {
    let maskElement;
    if (showOverlay) {
      maskElement = (
        <CSSTransition
          in={visible}
          appear
          timeout={transitionTime}
          classNames={`${prefixCls}-fade`}
          mountOnEnter
          unmountOnExit
          nodeRef={maskRef}
        >
          <div ref={maskRef} className={`${prefixCls}__mask`} />
        </CSSTransition>
      );
    }
    return maskElement;
  };
  const render = () => {
    const style: CSSProperties = {};
    if (visible) {
      style.display = 'block';
    }
    const wrapStyle = {
      ...style,
      zIndex,
    };
    const dialogBody = renderDialog();
    const wrapClass = classnames(
      props.className,
      `${prefixCls}__ctx`,
      !isNormal ? `${prefixCls}__ctx--fixed` : '',
      visible ? dialogOpenClass : '',
      isModal && showInAttachedElement ? `${prefixCls}__ctx--absolute` : '',
      props.mode === 'modeless' ? `${prefixCls}__ctx--modeless` : '',
    );
    // 如果不是 modal 模式 默認沒有 mask 也就沒有相關點擊 mask 事件
    const dialog = (
      <div ref={wrap} className={wrapClass} style={wrapStyle} onKeyDown={handleKeyDown} tabIndex={0}>
        {mode === 'modal' && renderMask()}
        {dialogBody}
      </div>
    );
    let dom = null;
    if (visible || wrap.current) {
      // normal 模式 attach 無效
      if (attach === '' || isNormal) {
        dom = dialog;
      } else {
        dom = (
          <CSSTransition
            in={visible}
            appear
            timeout={transitionTime}
            mountOnEnter
            unmountOnExit={destroyOnClose}
            nodeRef={portalRef}
          >
            <Portal attach={attach} ref={portalRef}>
              {dialog}
            </Portal>
          </CSSTransition>
        );
      }
    }
    return dom;
  };
  return render();
});
RenderDialog.defaultProps = dialogDefaultProps;
export default RenderDialog;

結束,react組件庫繼續(xù)搞起!

以上就是比ant更豐富Modal組件功能實現(xiàn)示例詳解的詳細內容,更多關于ant Modal組件功能的資料請關注腳本之家其它相關文章!

相關文章

  • 解決React報錯Property 'X' does not exist on type 'HTMLElement'

    解決React報錯Property 'X' does not 

    這篇文章主要為大家介紹了解決React報錯Property 'X' does not exist on type 'HTMLElement',有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-12-12
  • React native ListView 增加頂部下拉刷新和底下點擊刷新示例

    React native ListView 增加頂部下拉刷新和底下點擊刷新示例

    這篇文章主要介紹了React native ListView 增加頂部下拉刷新和底下點擊刷新示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-04-04
  • 比ant更豐富Modal組件功能實現(xiàn)示例詳解

    比ant更豐富Modal組件功能實現(xiàn)示例詳解

    這篇文章主要為大家介紹了比ant更豐富Modal組件功能實現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-11-11
  • 解讀react的onClick自動觸發(fā)等相關問題

    解讀react的onClick自動觸發(fā)等相關問題

    這篇文章主要介紹了解讀react的onClick自動觸發(fā)等相關問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-02-02
  • React useMemo與useCallabck有什么區(qū)別

    React useMemo與useCallabck有什么區(qū)別

    useCallback和useMemo是一樣的東西,只是入?yún)⒂兴煌?,useCallback緩存的是回調函數(shù),如果依賴項沒有更新,就會使用緩存的回調函數(shù);useMemo緩存的是回調函數(shù)的return,如果依賴項沒有更新,就會使用緩存的return
    2022-12-12
  • React?TypeScript?應用中便捷使用Redux?Toolkit方法詳解

    React?TypeScript?應用中便捷使用Redux?Toolkit方法詳解

    這篇文章主要為大家介紹了React?TypeScript?應用中便捷使用Redux?Toolkit方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-11-11
  • react render props模式實現(xiàn)組件復用示例

    react render props模式實現(xiàn)組件復用示例

    本文主要介紹了react render props模式實現(xiàn)組件復用示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2022-07-07
  • react ant Design手動設置表單的值操作

    react ant Design手動設置表單的值操作

    這篇文章主要介紹了react ant Design手動設置表單的值操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-10-10
  • 探究react-native 源碼的圖片緩存問題

    探究react-native 源碼的圖片緩存問題

    本篇文章主要介紹了探究react-native 源碼的圖片緩存問題,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2017-08-08
  • React?狀態(tài)管理工具優(yōu)劣勢示例分析

    React?狀態(tài)管理工具優(yōu)劣勢示例分析

    這篇文章主要為大家介紹了React?狀態(tài)管理工具優(yōu)劣勢示例分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-01-01

最新評論