Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/components/_util/dom.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isString } from 'lodash-es';
import { getCssVarsValue } from './style';
import type { AttachNode } from '../common';

// 用于判断是否可使用 dom
export const canUseDocument = !!(typeof window !== 'undefined' && window.document && window.document.createElement);
Expand All @@ -13,7 +14,7 @@ export const canUseDocument = !!(typeof window !== 'undefined' && window.documen
*/
export const isWindow = (val: unknown): val is Window => val === window;

export const getAttach = (node: any): HTMLElement => {
export const getAttach = (node: AttachNode): HTMLElement => {
const attachNode = typeof node === 'function' ? node() : node;
if (!attachNode) {
return document.body;
Expand Down
194 changes: 101 additions & 93 deletions packages/components/message/Message.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,30 @@
import React, { CSSProperties, useEffect } from 'react';
import React, { useEffect, useRef } from 'react';
import classNames from 'classnames';
import { getAttach } from '../_util/dom';
import noop from '../_util/noop';
import { render, unmount } from '../_util/react-render';
import PluginContainer from '../common/PluginContainer';
import ConfigProvider from '../config-provider';
import { getMessageConfig, globalConfig, setGlobalConfig } from './config';
import { PlacementOffset } from './const';
import MessageComponent from './MessageComponent';
import { useMessageClass } from './useMessageClass';

import {
import type {
MessageCloseAllMethod,
MessageConfigMethod,
MessageErrorMethod,
MessageInfoMethod,
MessageInstance,
MessageLoadingMethod,
MessageMethod,
MessageOptions,
MessagePlacementList,
MessageQuestionMethod,
MessageSuccessMethod,
MessageWarningMethod,
MessageThemeList,
MessageConfigMethod,
MessagePlacementList,
MessageWarningMethod,
} from './type';
import { AttachNodeReturnValue } from '../common';
import noop from '../_util/noop';
import { PlacementOffset } from './const';
import MessageComponent from './MessageComponent';

import { getMessageConfig, globalConfig, setGlobalConfig } from './config';
import { useMessageClass } from './useMessageClass';
import ConfigProvider from '../config-provider';
import PluginContainer from '../common/PluginContainer';

// 定义全局的 message 列表,closeAll 函数需要使用
let MessageList: MessageInstance[] = [];
let keyIndex = 1;

export interface MessagePlugin {
(theme: MessageThemeList, message: string | MessageOptions, duration?: number): Promise<MessageInstance>;
Expand All @@ -52,12 +47,22 @@ interface MessageContainerProps {
renderCallback?: Function;
}

interface ContainerInstance {
container: HTMLDivElement;
messages: MessageInstance[];
}

let messageKey = 1;

// 不同 attach 和 placement 对应的消息容器
const MessageContainerMaps: Map<HTMLElement, Map<MessagePlacementList, ContainerInstance>> = new Map();

const MessageContainer: React.FC<MessageContainerProps> = (props) => {
const { placement, children, zIndex, id, renderCallback } = props;
const { placement, children, zIndex, renderCallback } = props;

const style: CSSProperties = {
zIndex,
};
const ref = useRef<HTMLDivElement>(null);

const style: React.CSSProperties = { zIndex };

Object.keys(PlacementOffset[placement]).forEach((key) => {
style[key] = PlacementOffset[placement][key];
Expand All @@ -68,91 +73,89 @@ const MessageContainer: React.FC<MessageContainerProps> = (props) => {
}

useEffect(() => {
renderCallback();
renderCallback?.(ref.current);
// eslint-disable-next-line
}, []);

const { tdMessagePlacementClassGenerator, tdMessageListClass } = useMessageClass();

return (
<div className={classNames(tdMessageListClass, tdMessagePlacementClassGenerator(placement))} style={style} id={id}>
<div
ref={ref}
className={classNames(tdMessageListClass, tdMessagePlacementClassGenerator(placement))}
style={style}
>
{children}
</div>
);
};

function getAttachNodeMap(attachNode: HTMLElement) {
if (!MessageContainerMaps.has(attachNode)) {
MessageContainerMaps.set(attachNode, new Map());
}
return MessageContainerMaps.get(attachNode);
}

async function findExistingContainer(attachNode: HTMLElement, placement: MessagePlacementList, zIndex?: number) {
const attachNodeMap = getAttachNodeMap(attachNode);
let containerInstance = attachNodeMap.get(placement);
if (!containerInstance) {
const container = await createContainer({ zIndex, placement });
attachNode.appendChild(container);
containerInstance = {
container,
messages: [],
};
attachNodeMap.set(placement, containerInstance);
}
return containerInstance;
}

/**
* @desc 创建容器,所有的 message 会填充到容器中
*/
function createContainer({ attach, zIndex, placement = 'top' }: MessageOptions): Promise<Element> {
function createContainer({ zIndex, placement }): Promise<HTMLDivElement> {
return new Promise((resolve) => {
// 默认注入到 body 中,如果用户有指定,以用户指定的为准
let mountedDom: AttachNodeReturnValue = document.body;

// attach 为字符串时认为是选择器
if (typeof attach === 'string') {
const result = document.querySelectorAll(attach);
if (result.length >= 1) {
// :todo 编译器提示 nodelist 为类数组类型,并没有实现迭代器,没办法使用数组解构,暂时加上 eslint-disable
// eslint-disable-next-line prefer-destructuring
mountedDom = result[0];
}
} else if (typeof attach === 'function') {
mountedDom = attach();
}

// 选择器找到一个挂载 message 的容器,不存在则创建
const containerId = `tdesign-message-container--${placement}`;
const container = Array.from(mountedDom.querySelectorAll(`#${containerId}`));
if (container.length < 1) {
const div = document.createElement('div');
const mGlobalConfig = ConfigProvider.getGlobalConfig();

render(
<PluginContainer globalConfig={mGlobalConfig}>
<MessageContainer
id={containerId}
placement={placement}
zIndex={zIndex}
renderCallback={() => {
mountedDom.appendChild(div);
const container = Array.from(mountedDom.querySelectorAll(`#${containerId}`));
resolve(container[0]);
}}
/>
</PluginContainer>,
div,
);
} else {
resolve(container[0]);
}
const mGlobalConfig = ConfigProvider.getGlobalConfig();
const fragment = document.createDocumentFragment(); // 临时容器
render(
<PluginContainer globalConfig={mGlobalConfig}>
<MessageContainer
placement={placement}
zIndex={zIndex}
renderCallback={(element) => {
if (element) {
resolve(element);
}
}}
/>
</PluginContainer>,
fragment,
);
});
}

/**
* @desc 函数式调用时的 message 渲染函数
*/
async function renderElement(theme, config: MessageOptions): Promise<MessageInstance> {
const container = (await createContainer(config)) as HTMLElement;

const { content, offset, onClose = noop } = config;
const div = document.createElement('div');
const { attach, placement = 'top', zIndex, content, offset, onClose = noop } = config;

keyIndex += 1;
const attachNode = getAttach(attach);
const containerInstance = await findExistingContainer(attachNode, placement, zIndex);
const messageDiv = document.createElement('div');

const message = {
const message: MessageInstance = {
close: () => {
unmount(div);
div.remove();
message.closed = true;
// 关闭消息实例时,从全局的消息列表中移除该实例
const index = MessageList.indexOf(message);
if (index >= 0) {
MessageList.splice(index, 1);
if (messageDiv.parentNode) {
unmount(messageDiv);
messageDiv.remove();
}
const index = containerInstance.messages.indexOf(message);
if (index === -1) return;
containerInstance.messages.splice(index, 1);
},
key: keyIndex,
closed: false,
};

let style: React.CSSProperties = { ...config.style };
Expand All @@ -166,18 +169,18 @@ async function renderElement(theme, config: MessageOptions): Promise<MessageInst
};
}

messageKey += 1;
return new Promise((resolve) => {
/**
* message plugin 调用时走的渲染逻辑
* 调用获取全局上下文的方法获取信息,可传递当前组件自身信息(ConfigProvider.getGlobalConfig({message:config}))
* message组件不用穿,自身的配置信息都在props中
*/
const mGlobalConfig = ConfigProvider.getGlobalConfig();
// 渲染组件
render(
<PluginContainer globalConfig={mGlobalConfig}>
<MessageComponent
key={keyIndex}
key={messageKey}
{...config}
theme={theme}
style={style}
Expand All @@ -189,14 +192,10 @@ async function renderElement(theme, config: MessageOptions): Promise<MessageInst
{content}
</MessageComponent>
</PluginContainer>,
div,
messageDiv,
);

// 将当前渲染的 message 挂载到指定的容器中
container.appendChild(div);
// message 推入 message 列表
MessageList.push(message);
// 将 message 实例通过 resolve 返回给 promise 调用方
containerInstance.container.appendChild(messageDiv);
containerInstance.messages.push(message);
resolve(message);
});
}
Expand Down Expand Up @@ -234,7 +233,6 @@ MessagePlugin.loading = (content, duration) => messageMethod('loading', content,
MessagePlugin.config = (options: MessageOptions) => setGlobalConfig(options);

/**
* @date 2021-05-16 13:11:24
* @desc Message 顶层内置函数,传入 message promise,关闭传入的 message.
*/
MessagePlugin.close = (messageInstance) => {
Expand All @@ -245,10 +243,20 @@ MessagePlugin.close = (messageInstance) => {
* @desc 关闭所有的 message
*/
MessagePlugin.closeAll = (): MessageCloseAllMethod => {
MessageList.forEach((message) => {
typeof message.close === 'function' && message.close();
const allMessages: MessageInstance[] = [];
MessageContainerMaps.forEach((placementMap) => {
placementMap.forEach((instance) => {
// 收集需要关闭的消息实例,避免同时遍历与删除导致的索引错乱问题
allMessages.push(...instance.messages.slice());
});
});

// 批量关闭所有消息
allMessages.forEach((message) => {
if (typeof message.close === 'function') {
message.close();
}
});
MessageList = [];
return;
};

Expand Down
13 changes: 6 additions & 7 deletions packages/components/popup/PopupPlugin.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
import { CSSTransition } from 'react-transition-group';
import { createPopper, Instance, Placement, type Options } from '@popperjs/core';
import classNames from 'classnames';
import { isString } from 'lodash-es';
import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
import { CSSTransition } from 'react-transition-group';
import { getAttach } from '../_util/dom';
import { off, on } from '../_util/listener';
import { render, unmount } from '../_util/react-render';
import type { TNode } from '../common';
import PluginContainer from '../common/PluginContainer';
import ConfigProvider from '../config-provider';
import useDefaultProps from '../hooks/useDefaultProps';
import { popupDefaultProps } from './defaultProps';

import type { AttachNode, TNode } from '../common';
import type { TdPopupProps } from './type';

export interface PopupPluginApi {
config: TdPopupProps;
}

type TriggerEl = string | HTMLElement;

export interface OverlayProps extends TdPopupProps {
triggerEl: TriggerEl;
triggerEl: AttachNode;
renderCallback: (instance: HTMLElement) => void;
}

Expand Down Expand Up @@ -172,7 +171,7 @@ function removeOverlayInstance() {
}
}

export type PluginMethod = (triggerEl: TriggerEl, content: TNode, popupProps?: TdPopupProps) => Promise<Instance>;
export type PluginMethod = (triggerEl: AttachNode, content: TNode, popupProps?: TdPopupProps) => Promise<Instance>;

const renderInstance = (props, attach: HTMLElement): Promise<HTMLElement> =>
new Promise((resolve) => {
Expand Down
Loading