mirror of https://github.com/msgbyte/tailchat
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
207 lines
5.2 KiB
TypeScript
207 lines
5.2 KiB
TypeScript
import React, {
|
|
useCallback,
|
|
useRef,
|
|
Fragment,
|
|
useContext,
|
|
PropsWithChildren,
|
|
} from 'react';
|
|
import { useEffect } from 'react';
|
|
import { PortalManager } from './Manager';
|
|
import { createPortalContext } from './context';
|
|
import { PortalConsumer } from './Consumer';
|
|
import _isNil from 'lodash/isNil';
|
|
import { DefaultEventEmitter } from './defaultEventEmitter';
|
|
|
|
type Operation =
|
|
| { type: 'mount'; key: number; children: React.ReactNode }
|
|
| { type: 'update'; key: number; children: React.ReactNode }
|
|
| { type: 'unmount'; key: number };
|
|
|
|
// Events const
|
|
const addType = 'ADD_PORTAL';
|
|
const removeType = 'REMOVE_PORTAL';
|
|
|
|
// For react-native
|
|
// const TopViewEventEmitter = DeviceEventEmitter || new NativeEventEmitter();
|
|
|
|
const defaultRenderManagerViewFn = (children: React.ReactNode) => (
|
|
<>{children}</>
|
|
);
|
|
|
|
interface EventEmitterFunc {
|
|
emit: (...args: any[]) => any;
|
|
addListener: (...args: any[]) => any;
|
|
removeListener: (...args: any[]) => any;
|
|
}
|
|
export interface BuildPortalOptions {
|
|
/**
|
|
* 唯一标识名
|
|
* 用于多实例的情况
|
|
*/
|
|
hostName?: string;
|
|
|
|
/**
|
|
* 事件监听函数
|
|
*/
|
|
eventEmitter?: EventEmitterFunc;
|
|
|
|
/**
|
|
* 负责Portal Manager如何生成函数的逻辑
|
|
*/
|
|
renderManagerView?: (children: React.ReactNode) => React.ReactElement;
|
|
}
|
|
export function buildPortal(options: BuildPortalOptions) {
|
|
const {
|
|
hostName = 'default',
|
|
eventEmitter = new DefaultEventEmitter(),
|
|
renderManagerView = defaultRenderManagerViewFn,
|
|
} = options;
|
|
let nextKey = 10000;
|
|
|
|
const add = (el: React.ReactNode): number => {
|
|
const key = nextKey++;
|
|
eventEmitter.emit(addType, hostName, el, key);
|
|
return key;
|
|
};
|
|
|
|
const remove = (key: number): void => {
|
|
eventEmitter.emit(removeType, hostName, key);
|
|
};
|
|
|
|
const PortalContext = createPortalContext(hostName);
|
|
|
|
const PortalHost: React.FC<PropsWithChildren> = React.memo((props) => {
|
|
const managerRef = useRef<PortalManager>();
|
|
const nextKeyRef = useRef<number>(0);
|
|
const queueRef = useRef<Operation[]>([]);
|
|
const hostNameRef = useRef(hostName);
|
|
useEffect(() => {
|
|
hostNameRef.current = hostName;
|
|
}, [hostName]);
|
|
|
|
const mount: any = useCallback(
|
|
(name: string, children: React.ReactNode, _key?: number) => {
|
|
if (name !== hostNameRef.current) {
|
|
return;
|
|
}
|
|
|
|
const key = _key || nextKeyRef.current++;
|
|
if (managerRef.current) {
|
|
managerRef.current.mount(key, children);
|
|
} else {
|
|
queueRef.current.push({ type: 'mount', key, children });
|
|
}
|
|
|
|
return key;
|
|
},
|
|
[]
|
|
);
|
|
|
|
const update = useCallback(
|
|
(name: string, key: number, children: React.ReactNode) => {
|
|
if (name !== hostNameRef.current) {
|
|
return;
|
|
}
|
|
|
|
if (managerRef.current) {
|
|
managerRef.current.update(key, children);
|
|
} else {
|
|
const op: Operation = { type: 'mount', key, children };
|
|
const index = queueRef.current.findIndex(
|
|
(o) => o.type === 'mount' || (o.type === 'update' && o.key === key)
|
|
);
|
|
|
|
if (index > -1) {
|
|
queueRef.current[index] = op;
|
|
} else {
|
|
queueRef.current.push(op);
|
|
}
|
|
}
|
|
},
|
|
[]
|
|
);
|
|
|
|
const unmount = useCallback((name: string, key: number) => {
|
|
if (name !== hostNameRef.current) {
|
|
return;
|
|
}
|
|
|
|
if (managerRef.current) {
|
|
managerRef.current.unmount(key);
|
|
} else {
|
|
queueRef.current.push({ type: 'unmount', key });
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
eventEmitter.addListener(addType, mount);
|
|
eventEmitter.addListener(removeType, unmount);
|
|
|
|
return () => {
|
|
eventEmitter.removeListener(addType, mount);
|
|
eventEmitter.removeListener(removeType, unmount);
|
|
};
|
|
}, [mount, unmount]);
|
|
|
|
useEffect(() => {
|
|
// 处理队列
|
|
const queue = queueRef.current;
|
|
const manager = managerRef.current;
|
|
|
|
while (queue.length && manager) {
|
|
const action = queue.pop();
|
|
if (!action) {
|
|
continue;
|
|
}
|
|
|
|
switch (action.type) {
|
|
case 'mount':
|
|
manager.mount(action.key, action.children);
|
|
break;
|
|
case 'update':
|
|
manager.update(action.key, action.children);
|
|
break;
|
|
case 'unmount':
|
|
manager.unmount(action.key);
|
|
break;
|
|
}
|
|
}
|
|
}, []);
|
|
|
|
return (
|
|
<PortalContext.Provider
|
|
value={{
|
|
mount,
|
|
update,
|
|
unmount,
|
|
}}
|
|
>
|
|
<Fragment>{props.children}</Fragment>
|
|
<PortalManager
|
|
ref={managerRef as any}
|
|
renderManagerView={renderManagerView}
|
|
/>
|
|
</PortalContext.Provider>
|
|
);
|
|
});
|
|
PortalHost.displayName = 'PortalHost-' + hostName;
|
|
|
|
const PortalRender: React.FC<PropsWithChildren> = React.memo((props) => {
|
|
const manager = useContext(PortalContext);
|
|
|
|
if (_isNil(manager)) {
|
|
console.error('Not find PortalContext');
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<PortalConsumer hostName={hostName} manager={manager}>
|
|
{props.children}
|
|
</PortalConsumer>
|
|
);
|
|
});
|
|
PortalRender.displayName = 'PortalRender-' + hostName;
|
|
|
|
return { add, remove, PortalHost, PortalRender };
|
|
}
|