1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-07 01:41:39 +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

@@ -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",

View File

@@ -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",

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

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