index.tsx 7.6 KB

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