index.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. import React, { CSSProperties } from 'react';
  2. import ReactDOM from 'react-dom';
  3. import PropTypes from 'prop-types';
  4. import ToastListFoundation, {
  5. ToastListAdapter,
  6. ToastListProps,
  7. ToastListState
  8. } from '@douyinfe/semi-foundation/toast/toastListFoundation';
  9. import { cssClasses, strings } from '@douyinfe/semi-foundation/toast/constants';
  10. import BaseComponent from '../_base/baseComponent';
  11. import Toast from './toast';
  12. import '@douyinfe/semi-foundation/toast/toast.scss';
  13. import getUuid from '@douyinfe/semi-foundation/utils/uuid';
  14. import useToast from './useToast';
  15. import { ConfigProps, ToastInstance, ToastProps, ToastState } from '@douyinfe/semi-foundation/toast/toastFoundation';
  16. import CSSAnimation from '../_cssAnimation';
  17. import cls from 'classnames';
  18. export interface ToastReactProps extends ToastProps{
  19. id?: string;
  20. style?: CSSProperties;
  21. icon?: React.ReactNode;
  22. content: React.ReactNode
  23. }
  24. export type {
  25. ConfigProps,
  26. ToastListProps,
  27. ToastListState,
  28. ToastState
  29. };
  30. const createBaseToast = () => class ToastList extends BaseComponent<ToastListProps, ToastListState> {
  31. static ref: ToastList;
  32. static useToast: typeof useToast;
  33. static defaultOpts: ToastReactProps & { motion: boolean } = {
  34. motion: true,
  35. zIndex: 1010,
  36. content: '',
  37. };
  38. static propTypes = {
  39. content: PropTypes.node,
  40. duration: PropTypes.number,
  41. onClose: PropTypes.func,
  42. icon: PropTypes.node,
  43. direction: PropTypes.oneOf(strings.directions),
  44. stack: PropTypes.bool,
  45. };
  46. static defaultProps = {};
  47. static wrapperId: null | string;
  48. stack: boolean = false;
  49. innerWrapperRef: React.RefObject<HTMLDivElement> = React.createRef();
  50. constructor(props: ToastListProps) {
  51. super(props);
  52. this.state = {
  53. list: [],
  54. removedItems: [],
  55. updatedItems: [],
  56. mouseInSide: false
  57. };
  58. this.foundation = new ToastListFoundation(this.adapter);
  59. }
  60. get adapter(): ToastListAdapter {
  61. return {
  62. ...super.adapter,
  63. updateToast: (list: ToastInstance[], removedItems: ToastInstance[], updatedItems: ToastInstance[]) => {
  64. this.setState({ list, removedItems, updatedItems });
  65. },
  66. handleMouseInSideChange: (mouseInSide: boolean) => {
  67. this.setState({ mouseInSide });
  68. },
  69. getInputWrapperRect: () => {
  70. return this.innerWrapperRef.current?.getBoundingClientRect();
  71. }
  72. };
  73. }
  74. handleMouseEnter = (e: React.MouseEvent) => {
  75. if (this.stack) {
  76. this.foundation.handleMouseInSideChange(true);
  77. }
  78. }
  79. handleMouseLeave = (e: React.MouseEvent) => {
  80. if (this.stack) {
  81. const height = this.foundation.getInputWrapperRect()?.height;
  82. if (height) {
  83. this.foundation.handleMouseInSideChange(false);
  84. }
  85. }
  86. }
  87. static create(opts: ToastReactProps) {
  88. const id = opts.id ?? getUuid('toast');
  89. // this.id = id;
  90. if (!ToastList.ref) {
  91. const div = document.createElement('div');
  92. if (!this.wrapperId) {
  93. this.wrapperId = getUuid('toast-wrapper').slice(0, 26);
  94. }
  95. div.className = cssClasses.WRAPPER;
  96. div.id = this.wrapperId;
  97. div.style.zIndex = String(typeof opts.zIndex === 'number' ?
  98. opts.zIndex : ToastList.defaultOpts.zIndex);
  99. ['top', 'left', 'bottom', 'right'].map(pos => {
  100. if (pos in ToastList.defaultOpts || pos in opts) {
  101. const val = opts[pos] ? opts[pos] : ToastList.defaultOpts[pos];
  102. div.style[pos] = typeof val === 'number' ? `${val}px` : val;
  103. }
  104. });
  105. // document.body.appendChild(div);
  106. if (ToastList.defaultOpts.getPopupContainer) {
  107. const container = ToastList.defaultOpts.getPopupContainer();
  108. container.appendChild(div);
  109. } else {
  110. document.body.appendChild(div);
  111. }
  112. ReactDOM.render(React.createElement(
  113. ToastList,
  114. { ref: instance => (ToastList.ref = instance) }
  115. ),
  116. div,
  117. () => {
  118. ToastList.ref.add({ ...opts, id });
  119. ToastList.ref.stack = Boolean(opts.stack);
  120. });
  121. } else {
  122. const node = document.querySelector(`#${this.wrapperId}`) as HTMLElement;
  123. ['top', 'left', 'bottom', 'right'].map(pos => {
  124. if (pos in opts) {
  125. node.style[pos] = typeof opts[pos] === 'number' ? `${opts[pos]}px` : opts[pos];
  126. }
  127. });
  128. if (Boolean(opts.stack) !== ToastList.ref.stack) {
  129. ToastList.ref.stack = Boolean(opts.stack);
  130. }
  131. if (ToastList.ref.has(id)) {
  132. ToastList.ref.update(id, { ...opts, id });
  133. } else {
  134. ToastList.ref.add({ ...opts, id });
  135. }
  136. }
  137. return id;
  138. }
  139. static close(id: string) {
  140. if (ToastList.ref) {
  141. ToastList.ref.remove(id);
  142. }
  143. }
  144. static destroyAll() {
  145. if (ToastList.ref) {
  146. ToastList.ref.destroyAll();
  147. const wrapper = document.querySelector(`#${this.wrapperId}`);
  148. ReactDOM.unmountComponentAtNode(wrapper);
  149. wrapper && wrapper.parentNode.removeChild(wrapper);
  150. ToastList.ref = null;
  151. this.wrapperId = null;
  152. }
  153. }
  154. static getWrapperId() {
  155. return this.wrapperId;
  156. }
  157. static info(opts: Omit<ToastReactProps, 'type'> | string) {
  158. if (typeof opts === 'string') {
  159. opts = { content: opts };
  160. }
  161. return this.create({ ...ToastList.defaultOpts, ...opts, type: 'info' });
  162. }
  163. static warning(opts: Omit<ToastReactProps, 'type'> | string) {
  164. if (typeof opts === 'string') {
  165. opts = { content: opts };
  166. }
  167. return this.create({ ...ToastList.defaultOpts, ...opts, type: 'warning' });
  168. }
  169. static error(opts: Omit<ToastReactProps, 'type'> | string) {
  170. if (typeof opts === 'string') {
  171. opts = { content: opts };
  172. }
  173. return this.create({ ...ToastList.defaultOpts, ...opts, type: 'error' });
  174. }
  175. static success(opts: Omit<ToastReactProps, 'type'> | string) {
  176. if (typeof opts === 'string') {
  177. opts = { content: opts };
  178. }
  179. return this.create({ ...ToastList.defaultOpts, ...opts, type: 'success' });
  180. }
  181. static config(opts: ConfigProps) {
  182. ['top', 'left', 'bottom', 'right'].forEach(pos => {
  183. if (pos in opts) {
  184. ToastList.defaultOpts[pos] = opts[pos];
  185. }
  186. });
  187. if (typeof opts.theme === 'string' && strings.themes.includes(opts.theme)) {
  188. ToastList.defaultOpts.theme = opts.theme;
  189. }
  190. if (typeof opts.zIndex === 'number') {
  191. ToastList.defaultOpts.zIndex = opts.zIndex;
  192. }
  193. if (typeof opts.duration === 'number') {
  194. ToastList.defaultOpts.duration = opts.duration;
  195. }
  196. if (typeof opts.getPopupContainer === 'function') {
  197. ToastList.defaultOpts.getPopupContainer = opts.getPopupContainer;
  198. }
  199. }
  200. has(id: string) {
  201. return this.foundation.hasToast(id);
  202. }
  203. add(opts: ToastInstance) {
  204. return this.foundation.addToast(opts);
  205. }
  206. update(id: string, opts: ToastInstance) {
  207. return this.foundation.updateToast(id, opts);
  208. }
  209. remove(id: string) {
  210. return this.foundation.removeToast(id);
  211. }
  212. destroyAll() {
  213. return this.foundation.destroyAll();
  214. }
  215. render() {
  216. let { list } = this.state;
  217. const { removedItems, updatedItems } = this.state;
  218. list = Array.from(new Set([...list, ...removedItems]));
  219. const updatedIds = updatedItems.map(({ id }) => id);
  220. const refFn: React.LegacyRef<Toast> = (toast) => {
  221. if (toast?.foundation?._id && updatedIds.includes(toast.foundation._id)) {
  222. toast.foundation.restartCloseTimer();
  223. }
  224. };
  225. return (
  226. <React.Fragment>
  227. <div className={cls({
  228. [`${cssClasses.PREFIX}-innerWrapper`]: true,
  229. [`${cssClasses.PREFIX}-innerWrapper-hover`]: this.state.mouseInSide
  230. })} ref={this.innerWrapperRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
  231. {list.map((item, index) =>{
  232. const isRemoved = removedItems.find(removedItem=>removedItem.id===item.id) !== undefined;
  233. return <CSSAnimation key={item.id} motion={item.motion} animationState={isRemoved?"leave":"enter"} startClassName={isRemoved?`${cssClasses.PREFIX}-animation-hide`:`${cssClasses.PREFIX}-animation-show`}>
  234. {
  235. ({ animationClassName, animationEventsNeedBind, isAnimating })=>{
  236. return (isRemoved && !isAnimating) ? null : <Toast {...item} stack={this.stack} stackExpanded={this.state.mouseInSide} positionInList={{ length: list.length, index }} className={cls({
  237. [item.className]: Boolean(item.className),
  238. [animationClassName]: true
  239. })} {...animationEventsNeedBind} style={{ ...item.style }} close={id => this.remove(id)} ref={refFn} />;
  240. }
  241. }
  242. </CSSAnimation>;
  243. }
  244. )}
  245. </div>
  246. </React.Fragment>
  247. );
  248. }
  249. };
  250. export class ToastFactory {
  251. static create(config?: ConfigProps): ReturnType<typeof createBaseToast> {
  252. const newToast = createBaseToast();
  253. newToast.useToast = useToast;
  254. config && newToast.config(config);
  255. return newToast;
  256. }
  257. }
  258. export default ToastFactory.create();