1
0

index.tsx 8.5 KB

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