1
0

index.tsx 7.6 KB

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