1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-05 08:53:14 +00:00

fix toast

This commit is contained in:
cainiao222
2018-12-07 12:59:25 +08:00
parent 0df3066b06
commit ef838eab4d
10 changed files with 547 additions and 174 deletions

View 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>&times;</span>
</div>
</div>
);
}
}
export default Alert;

View File

@@ -1,3 +1,5 @@
import Toast from './toast';
import Toaster from './toaster';
export default Toast;
const toaster = new Toaster();
export default toaster;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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%;
}

View File

@@ -1,40 +1,197 @@
import React from 'react';
import ReactDOM from 'react-dom';
import NoticeContainer from './notice-container';
import './toast.css';
import { css } from 'glamor';
import PropTypes from 'prop-types';
import Transition from 'react-transition-group/Transition';
import Alert from './alert';
function createNotieContainer() {
const div = document.createElement('div');
document.body.appendChild(div);
const noticeContainer = ReactDOM.render(<NoticeContainer />, div);
return {
addNotice(notice) {
return noticeContainer.addNotice(notice);
},
destroy() {
ReactDOM.unmountComponentAtNode(div);
document.body.removeChild(div);
}
};
}
let noticeContainer = null;
const notice = (type, content, duration = 2000, onClose) => {
if (!noticeContainer) noticeContainer = createNotieContainer();
return noticeContainer.addNotice({ type, content, duration, onClose });
const animationEasing = {
deceleration: 'cubic-bezier(0.0, 0.0, 0.2, 1)',
acceleration: 'cubic-bezier(0.4, 0.0, 1, 1)',
spring: 'cubic-bezier(0.175, 0.885, 0.320, 1.175)'
};
export default {
info(content, duration, onClose) {
return notice('info', content, duration, onClose);
const ANIMATION_DURATION = 240;
const openAnimation = css.keyframes('openAnimation', {
from: {
opacity: 0,
transform: 'translateY(-120%)'
},
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);
to: {
transform: 'translateY(0)'
}
};
});
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>
);
}
}

View 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>
);
}
}

View 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' });
}
}