mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-07 01:41:39 +00:00
fix toast
This commit is contained in:
47
frontend/package-lock.json
generated
47
frontend/package-lock.json
generated
@@ -2002,6 +2002,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.1.3.tgz",
|
||||||
"integrity": "sha512-rDFIzgXcof0jDyjNosjv4Sno77X4KuPeFxG2XZZv1/Kc8DRVGVADdoQyyOVDwPqL36DDmtCQbrpMCqvpPLJQ0w=="
|
"integrity": "sha512-rDFIzgXcof0jDyjNosjv4Sno77X4KuPeFxG2XZZv1/Kc8DRVGVADdoQyyOVDwPqL36DDmtCQbrpMCqvpPLJQ0w=="
|
||||||
},
|
},
|
||||||
|
"bowser": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/bowser/-/bowser-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ=="
|
||||||
|
},
|
||||||
"boxen": {
|
"boxen": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz",
|
||||||
@@ -2858,6 +2863,22 @@
|
|||||||
"resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz",
|
||||||
"integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA="
|
"integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA="
|
||||||
},
|
},
|
||||||
|
"css-in-js-utils": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "http://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA==",
|
||||||
|
"requires": {
|
||||||
|
"hyphenate-style-name": "^1.0.2",
|
||||||
|
"isobject": "^3.0.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"isobject": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
|
||||||
|
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"css-loader": {
|
"css-loader": {
|
||||||
"version": "0.28.7",
|
"version": "0.28.7",
|
||||||
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-0.28.7.tgz",
|
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-0.28.7.tgz",
|
||||||
@@ -5276,6 +5297,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"glamor": {
|
||||||
|
"version": "2.20.40",
|
||||||
|
"resolved": "https://registry.npmjs.org/glamor/-/glamor-2.20.40.tgz",
|
||||||
|
"integrity": "sha512-DNXCd+c14N9QF8aAKrfl4xakPk5FdcFwmH7sD0qnC0Pr7xoZ5W9yovhUrY/dJc3psfGGXC58vqQyRtuskyUJxA==",
|
||||||
|
"requires": {
|
||||||
|
"fbjs": "^0.8.12",
|
||||||
|
"inline-style-prefixer": "^3.0.6",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"prop-types": "^15.5.10",
|
||||||
|
"through": "^2.3.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"glob": {
|
"glob": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
||||||
@@ -5991,6 +6024,11 @@
|
|||||||
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
|
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"hyphenate-style-name": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-MRYKNpMK2vH8BMYHT360FGXU7Es="
|
||||||
|
},
|
||||||
"i18next": {
|
"i18next": {
|
||||||
"version": "11.3.2",
|
"version": "11.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-11.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/i18next/-/i18next-11.3.2.tgz",
|
||||||
@@ -6106,6 +6144,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
|
||||||
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
|
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
|
||||||
},
|
},
|
||||||
|
"inline-style-prefixer": {
|
||||||
|
"version": "3.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-3.0.8.tgz",
|
||||||
|
"integrity": "sha1-hVG45bTVcyROZqNLBPfTIHaitTQ=",
|
||||||
|
"requires": {
|
||||||
|
"bowser": "^1.7.3",
|
||||||
|
"css-in-js-utils": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"inquirer": {
|
"inquirer": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz",
|
||||||
|
@@ -13,6 +13,7 @@
|
|||||||
"dotenv": "4.0.0",
|
"dotenv": "4.0.0",
|
||||||
"dotenv-expand": "4.2.0",
|
"dotenv-expand": "4.2.0",
|
||||||
"file-loader": "1.1.5",
|
"file-loader": "1.1.5",
|
||||||
|
"glamor": "^2.20.40",
|
||||||
"html-webpack-plugin": "2.29.0",
|
"html-webpack-plugin": "2.29.0",
|
||||||
"jest": "20.0.4",
|
"jest": "20.0.4",
|
||||||
"moment": "^2.22.2",
|
"moment": "^2.22.2",
|
||||||
|
101
frontend/src/components/toast/alert.js
Normal file
101
frontend/src/components/toast/alert.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { css } from 'glamor';
|
||||||
|
|
||||||
|
class Alert extends React.PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.containerStyle = css({
|
||||||
|
borderRadius: '3px',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
padding: '20px',
|
||||||
|
display: 'flex',
|
||||||
|
boxShadow: 'rgba(67, 90, 111, 0.3) 0px 0px 1px, rgba(67, 90, 111, 0.47) 0px 8px 10px -4px',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
flexDirection: 'row',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.containerBorderSuccess = css({
|
||||||
|
borderLeft: '3px solid rgb(71, 184, 129)'
|
||||||
|
});
|
||||||
|
|
||||||
|
this.containerBorderWarn = css({
|
||||||
|
borderLeft: '3px solid rgb(217, 130, 43)'
|
||||||
|
});
|
||||||
|
|
||||||
|
this.containerBorderDanger = css({
|
||||||
|
borderLeft: '3px solid rgb(236, 76, 71)'
|
||||||
|
});
|
||||||
|
|
||||||
|
this.containerBorderNotify = css({
|
||||||
|
borderLeft: '3px solid rgb(16, 112, 202)'
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
this.toastTextTitle = css({
|
||||||
|
fontWeight: '600',
|
||||||
|
fontSize: '14px',
|
||||||
|
color: '#435a6f'
|
||||||
|
});
|
||||||
|
|
||||||
|
this.toastTextChild = css({
|
||||||
|
fontSize: '14px',
|
||||||
|
color: '#999'
|
||||||
|
});
|
||||||
|
this.toastClose = css({
|
||||||
|
marginLeft: '15px',
|
||||||
|
height: '24px',
|
||||||
|
width: '24px',
|
||||||
|
lineHeight: '22px',
|
||||||
|
fontWeight: '700',
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: '20px',
|
||||||
|
color: '#000',
|
||||||
|
cursor: 'pointer',
|
||||||
|
opacity: '0.5',
|
||||||
|
':hover': {
|
||||||
|
opacity: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.toastIcon = css({
|
||||||
|
marginRight: '10px',
|
||||||
|
width: '14px',
|
||||||
|
height: '20px',
|
||||||
|
lineHeight: '20px'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getContainerStyle(intent) {
|
||||||
|
switch (intent) {
|
||||||
|
case 'success':
|
||||||
|
return { borderStyle: this.containerBorderSuccess, iconColor: css({color: 'rgb(71, 184, 129)'}), iconClass: 'fa fa-check-circle' };
|
||||||
|
case 'warning':
|
||||||
|
return { borderStyle: this.containerBorderWarn, iconColor: css({color: 'rgb(217, 130, 43)'}) , iconClass: 'fa fa-exclamation-triangle' };
|
||||||
|
case 'none':
|
||||||
|
return { borderStyle: this.containerBorderNotify, iconColor: css({color: 'rgb(16, 112, 202)'}), iconClass: 'fa fa-exclamation-circle' };
|
||||||
|
case 'danger':
|
||||||
|
return { borderStyle: this.containerBorderDanger, iconColor: css({color: 'rgb(236, 76, 71)'}), iconClass: 'fa fa-exclamation-circle' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const toastStyle = this.getContainerStyle(this.props.intent);
|
||||||
|
return (
|
||||||
|
<div {...css(toastStyle.borderStyle, this.containerStyle)}>
|
||||||
|
<div className={this.toastIcon} >
|
||||||
|
<i className={toastStyle.iconClass} {...toastStyle.iconColor}/>
|
||||||
|
</div>
|
||||||
|
<div className={this.toastTextContainer}>
|
||||||
|
<p className={this.toastTextTitle}>{this.props.title}</p>
|
||||||
|
{ <p className={this.toastTextChild}>{this.props.children}</p> }
|
||||||
|
</div>
|
||||||
|
<div onClick={this.props.onRemove} className={this.toastClose}>
|
||||||
|
<span>×</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Alert;
|
@@ -1,3 +1,5 @@
|
|||||||
import Toast from './toast';
|
import Toaster from './toaster';
|
||||||
|
|
||||||
export default Toast;
|
const toaster = new Toaster();
|
||||||
|
|
||||||
|
export default toaster;
|
@@ -1,67 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
|
||||||
import Notice from './notice';
|
|
||||||
|
|
||||||
class NoticeContainer extends React.Component {
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.transitionTime = 300;
|
|
||||||
this.state = {notices: []};
|
|
||||||
}
|
|
||||||
|
|
||||||
addNotice = (notice) => {
|
|
||||||
const { notices } = this.state;
|
|
||||||
notice.key = this.getNoticeKey();
|
|
||||||
notices.push(notice);
|
|
||||||
this.setState({notices});
|
|
||||||
|
|
||||||
if (notice.duration > 0) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.removeNotice(notice.key);
|
|
||||||
}, notice.duration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeNotice = (key) => {
|
|
||||||
const { notices } = this.state;
|
|
||||||
this.setState({
|
|
||||||
notices: notices.filter((notice) => {
|
|
||||||
if (notice.key === key) {
|
|
||||||
if (notice.close) {
|
|
||||||
setTimeout(notice.close, this.transitionTime);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getNoticeKey = () => {
|
|
||||||
const { notices } = this.state;
|
|
||||||
return `notice-${new Date().getTime()}-${notices.length}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { notices } = this.state;
|
|
||||||
return (
|
|
||||||
<TransitionGroup className="toast-notification">
|
|
||||||
{notices.map((notice) => {
|
|
||||||
return (
|
|
||||||
<CSSTransition
|
|
||||||
key={notice.key}
|
|
||||||
classNames="toast-notice-wrapper notice"
|
|
||||||
timeout={this.transitionTime}
|
|
||||||
>
|
|
||||||
<Notice key={notice.key} {...notice} />
|
|
||||||
</CSSTransition>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TransitionGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default NoticeContainer;
|
|
@@ -1,22 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
type: PropTypes.string.isRequired,
|
|
||||||
content: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
class Notice extends React.Component {
|
|
||||||
render() {
|
|
||||||
let { type, content } = this.props;
|
|
||||||
return (
|
|
||||||
<div className="toast-notice">
|
|
||||||
<span className={`alert alert-${type}`}>{content}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Notice.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default Notice;
|
|
@@ -1,49 +0,0 @@
|
|||||||
.toast-notification {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast-notice-wrapper.notice-enter {
|
|
||||||
top: 0px;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast-notice-wrapper.notice-enter-active {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0.9125rem);
|
|
||||||
transition: all 300ms ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast-notice-wrapper.notice-exit {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast-notice-wrapper.notice-exit-active {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(-100%);
|
|
||||||
transition: all 300ms ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast-notice {
|
|
||||||
position: relative;
|
|
||||||
top: 0.9125rem;
|
|
||||||
margin:0 auto 0.9125rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
background: #FFFFFF;
|
|
||||||
box-shadow: 0px 10px 20px 0px rgba(0, 0, 0, .1);
|
|
||||||
border-radius: 6px;
|
|
||||||
color: #454545;
|
|
||||||
font-size: 0.9125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast-notice>span {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0.3125rem;
|
|
||||||
line-height: 100%;
|
|
||||||
}
|
|
@@ -1,40 +1,197 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import { css } from 'glamor';
|
||||||
import NoticeContainer from './notice-container';
|
import PropTypes from 'prop-types';
|
||||||
import './toast.css';
|
import Transition from 'react-transition-group/Transition';
|
||||||
|
import Alert from './alert';
|
||||||
|
|
||||||
function createNotieContainer() {
|
const animationEasing = {
|
||||||
const div = document.createElement('div');
|
deceleration: 'cubic-bezier(0.0, 0.0, 0.2, 1)',
|
||||||
document.body.appendChild(div);
|
acceleration: 'cubic-bezier(0.4, 0.0, 1, 1)',
|
||||||
const noticeContainer = ReactDOM.render(<NoticeContainer />, div);
|
spring: 'cubic-bezier(0.175, 0.885, 0.320, 1.175)'
|
||||||
return {
|
};
|
||||||
addNotice(notice) {
|
|
||||||
return noticeContainer.addNotice(notice);
|
const ANIMATION_DURATION = 240;
|
||||||
|
|
||||||
|
const openAnimation = css.keyframes('openAnimation', {
|
||||||
|
from: {
|
||||||
|
opacity: 0,
|
||||||
|
transform: 'translateY(-120%)'
|
||||||
},
|
},
|
||||||
destroy() {
|
to: {
|
||||||
ReactDOM.unmountComponentAtNode(div);
|
transform: 'translateY(0)'
|
||||||
document.body.removeChild(div);
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeAnimation = css.keyframes('closeAnimation', {
|
||||||
|
from: {
|
||||||
|
transform: 'scale(1)',
|
||||||
|
opacity: 1
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
transform: 'scale(0.9)',
|
||||||
|
opacity: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const animationStyles = css({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: 0,
|
||||||
|
transition: `all ${ANIMATION_DURATION}ms ${animationEasing.deceleration}`,
|
||||||
|
'&[data-state="entering"], &[data-state="entered"]': {
|
||||||
|
animation: `${openAnimation} ${ANIMATION_DURATION}ms ${
|
||||||
|
animationEasing.spring
|
||||||
|
} both`
|
||||||
|
},
|
||||||
|
'&[data-state="exiting"]': {
|
||||||
|
animation: `${closeAnimation} 120ms ${animationEasing.acceleration} both`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default class Toast extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
/**
|
||||||
|
* The z-index of the toast.
|
||||||
|
*/
|
||||||
|
zIndex: PropTypes.number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duration of the toast.
|
||||||
|
*/
|
||||||
|
duration: PropTypes.number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called when the toast is all the way closed.
|
||||||
|
*/
|
||||||
|
onRemove: PropTypes.func,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the alert.
|
||||||
|
*/
|
||||||
|
intent: PropTypes.oneOf(['none', 'success', 'warning', 'danger'])
|
||||||
|
.isRequired,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The title of the alert.
|
||||||
|
*/
|
||||||
|
title: PropTypes.node,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Description of the alert.
|
||||||
|
*/
|
||||||
|
children: PropTypes.node,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When true, show a close icon button inside of the toast.
|
||||||
|
*/
|
||||||
|
hasCloseButton: PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When false, will close the Toast and call onRemove when finished.
|
||||||
|
*/
|
||||||
|
isShown: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
intent: 'none'
|
||||||
|
}
|
||||||
|
|
||||||
|
state = {
|
||||||
|
isShown: true,
|
||||||
|
height: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (prevProps.isShown !== this.props.isShown) {
|
||||||
|
// eslint-disable-next-line react/no-did-update-set-state
|
||||||
|
this.setState({
|
||||||
|
isShown: this.props.isShown
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.startCloseTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.clearCloseTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
close = () => {
|
||||||
|
this.clearCloseTimer();
|
||||||
|
this.setState({
|
||||||
|
isShown: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startCloseTimer = () => {
|
||||||
|
if (this.props.duration) {
|
||||||
|
this.closeTimer = setTimeout(() => {
|
||||||
|
this.close();
|
||||||
|
}, this.props.duration * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCloseTimer = () => {
|
||||||
|
if (this.closeTimer) {
|
||||||
|
clearTimeout(this.closeTimer);
|
||||||
|
this.closeTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseEnter = () => {
|
||||||
|
this.clearCloseTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseLeave = () => {
|
||||||
|
this.startCloseTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
onRef = ref => {
|
||||||
|
if (ref === null) return;
|
||||||
|
|
||||||
|
const { height } = ref.getBoundingClientRect();
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
height
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Transition
|
||||||
|
appear
|
||||||
|
unmountOnExit
|
||||||
|
timeout={ANIMATION_DURATION}
|
||||||
|
in={this.state.isShown}
|
||||||
|
onExited={this.props.onRemove}
|
||||||
|
>
|
||||||
|
{state => (
|
||||||
|
<div
|
||||||
|
data-state={state}
|
||||||
|
className={animationStyles}
|
||||||
|
onMouseEnter={this.handleMouseEnter}
|
||||||
|
onMouseLeave={this.handleMouseLeave}
|
||||||
|
style={{
|
||||||
|
height: this.state.height,
|
||||||
|
zIndex: this.props.zIndex,
|
||||||
|
marginBottom: this.state.isShown ? 0 : -this.state.height
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div ref={this.onRef} style={{ padding: 8 }}>
|
||||||
|
<Alert
|
||||||
|
intent={this.props.intent}
|
||||||
|
title={this.props.title}
|
||||||
|
children={this.props.children}
|
||||||
|
isRemoveable={this.props.hasCloseButton}
|
||||||
|
onRemove={() => this.close()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Transition>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let noticeContainer = null;
|
|
||||||
const notice = (type, content, duration = 2000, onClose) => {
|
|
||||||
if (!noticeContainer) noticeContainer = createNotieContainer();
|
|
||||||
return noticeContainer.addNotice({ type, content, duration, onClose });
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
info(content, duration, onClose) {
|
|
||||||
return notice('info', content, duration, onClose);
|
|
||||||
},
|
|
||||||
success(content, duration, onClose) {
|
|
||||||
return notice('success', content, duration, onClose);
|
|
||||||
},
|
|
||||||
warning(content, duration, onClose) {
|
|
||||||
return notice('warning', content, duration, onClose);
|
|
||||||
},
|
|
||||||
error(content, duration, onClose) {
|
|
||||||
return notice('danger', content, duration, onClose);
|
|
||||||
}
|
|
||||||
};
|
|
138
frontend/src/components/toast/toastManager.js
Normal file
138
frontend/src/components/toast/toastManager.js
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { css } from 'glamor';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import Toast from './toast';
|
||||||
|
|
||||||
|
const wrapperClass = css({
|
||||||
|
maxWidth: 560,
|
||||||
|
margin: '0 auto',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
position: 'fixed',
|
||||||
|
zIndex: 30
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const hasCustomId = settings => Object.hasOwnProperty.call(settings, 'id');
|
||||||
|
|
||||||
|
export default class ToastManager extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
/**
|
||||||
|
* Function called with the `this.notify` function.
|
||||||
|
*/
|
||||||
|
bindNotify: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called with the `this.getToasts` function.
|
||||||
|
*/
|
||||||
|
bindGetToasts: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called with the `this.closeAll` function.
|
||||||
|
*/
|
||||||
|
bindCloseAll: PropTypes.func.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
static idCounter = 0;
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
props.bindNotify(this.notify);
|
||||||
|
props.bindGetToasts(this.getToasts);
|
||||||
|
props.bindCloseAll(this.closeAll);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
toasts: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getToasts = () => {
|
||||||
|
return this.state.toasts;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAll = () => {
|
||||||
|
this.getToasts().forEach(toast => toast.close());
|
||||||
|
}
|
||||||
|
|
||||||
|
notify = (title, settings) => {
|
||||||
|
// If there's a custom toast ID passed, close existing toasts with the same custom ID
|
||||||
|
if (hasCustomId(settings)) {
|
||||||
|
for (const toast of this.state.toasts) {
|
||||||
|
// Since unique ID is still appended to a custom ID, skip the unique ID and check only prefix
|
||||||
|
if (String(toast.id).startsWith(settings.id)) {
|
||||||
|
this.closeToast(toast.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = this.createToastInstance(title, settings);
|
||||||
|
|
||||||
|
this.setState(previousState => {
|
||||||
|
return {
|
||||||
|
toasts: [instance, ...previousState.toasts]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
createToastInstance = (title, settings) => {
|
||||||
|
const uniqueId = ++ToastManager.idCounter;
|
||||||
|
const id = hasCustomId(settings) ? `${settings.id}-${uniqueId}` : uniqueId;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
description: settings.description,
|
||||||
|
hasCloseButton: settings.hasCloseButton || true,
|
||||||
|
duration: settings.duration || 5,
|
||||||
|
close: () => this.closeToast(id),
|
||||||
|
intent: settings.intent
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will set isShown on the Toast which will close the toast.
|
||||||
|
* It won't remove the toast until onExited triggers onRemove.
|
||||||
|
*/
|
||||||
|
closeToast = id => {
|
||||||
|
this.setState(previousState => {
|
||||||
|
return {
|
||||||
|
toasts: previousState.toasts.map(toast => {
|
||||||
|
if (toast.id === id) {
|
||||||
|
return {
|
||||||
|
...toast,
|
||||||
|
isShown: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return toast;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeToast = id => {
|
||||||
|
this.setState(previousState => {
|
||||||
|
return {
|
||||||
|
toasts: previousState.toasts.filter(toast => toast.id !== id)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<span className={wrapperClass}>
|
||||||
|
{this.state.toasts.map(({ id, description, ...props }) => {
|
||||||
|
return (
|
||||||
|
<Toast key={id} onRemove={() => this.removeToast(id)} {...props}>
|
||||||
|
{description}
|
||||||
|
</Toast>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
65
frontend/src/components/toast/toaster.js
Normal file
65
frontend/src/components/toast/toaster.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import ToastManager from './toastManager';
|
||||||
|
|
||||||
|
const isBrowser =
|
||||||
|
typeof window !== 'undefined' && typeof window.document !== 'undefined';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Toaster manages the interactionsb between
|
||||||
|
* the ToasterManger and the toast API.
|
||||||
|
*/
|
||||||
|
export default class Toaster {
|
||||||
|
constructor() {
|
||||||
|
if (!isBrowser) return;
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.setAttribute('data-evergreen-toaster-container', '');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<ToastManager
|
||||||
|
bindNotify={this._bindNotify}
|
||||||
|
bindGetToasts={this._bindGetToasts}
|
||||||
|
bindCloseAll={this._bindCloseAll}
|
||||||
|
/>,
|
||||||
|
container
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_bindNotify = handler => {
|
||||||
|
this.notifyHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
_bindGetToasts = handler => {
|
||||||
|
this.getToastsHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
_bindCloseAll = handler => {
|
||||||
|
this.closeAllHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
getToasts = () => {
|
||||||
|
return this.getToastsHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAll = () => {
|
||||||
|
return this.closeAllHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
notify = (title, settings = {}) => {
|
||||||
|
return this.notifyHandler(title, { ...settings, intent: 'none' });
|
||||||
|
}
|
||||||
|
|
||||||
|
success = (title, settings = {}) => {
|
||||||
|
return this.notifyHandler(title, { ...settings, intent: 'success' });
|
||||||
|
}
|
||||||
|
|
||||||
|
warning = (title, settings = {}) => {
|
||||||
|
return this.notifyHandler(title, { ...settings, intent: 'warning' });
|
||||||
|
}
|
||||||
|
|
||||||
|
danger = (title, settings = {}) => {
|
||||||
|
return this.notifyHandler(title, { ...settings, intent: 'danger' });
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user