hc-components-test
Version:
基于react的通用组件库
258 lines (216 loc) • 7.07 kB
JSX
import React from 'react';
import {bool, number, object, string} from 'prop-types';
export const UPDATE_TIME = 200;
export const MAX_PROGRESS = 99;
export const PROGRESS_INCREASE = 10;
export const ANIMATION_TIME = UPDATE_TIME * 4;
export const TERMINATING_ANIMATION_TIME = UPDATE_TIME / 2;
const initialState = {
terminatingAnimationTimeout: null,
percent: 0,
progressInterval: null
};
const DEFAULT_SCOPE = 'default';
export class LoadingBar extends React.Component {
static instances = {};
static create = (opt = {}) => {
return (action, next) => {
if (action.type && !action.suppressGlobalProgress) {
if (action.type.match(/\/start$/)) {
LoadingBar.showLoading(opt.name);
} else if (action.type.match(/\/success$/) || action.type.match(/\/error$/)) {
LoadingBar.hideLoading(opt.name);
}
}
next(action);
};
}
static showLoading(scope = DEFAULT_SCOPE) {
const instance = LoadingBar.instances[scope];
if (instance && instance.handleUpdate) {
instance.handleUpdate({
loading: (instance.state.loading || 0) + 1
});
} else {
LoadingBar.instances[scope] = (LoadingBar.instances[scope] || 0) + 1;
}
}
static hideLoading(scope = DEFAULT_SCOPE) {
const instance = LoadingBar.instances[scope];
if (instance && instance.handleUpdate) {
instance.handleUpdate({
loading: Math.max(0, (instance.state.loading || 1) - 1)
});
}
}
static resetLoading(scope = DEFAULT_SCOPE) {
const instance = LoadingBar.instances[scope];
if (instance && instance.handleUpdate) {
instance.handleUpdate({loading: 0});
}
}
static propTypes = {
className: string,
loading: number,
maxProgress: number,
progressIncrease: number,
showFastActions: bool,
updateTime: number,
// eslint-disable-next-line react/no-unused-prop-types
scope: string,
// eslint-disable-next-line react/forbid-prop-types
style: object
}
static defaultProps = {
className: '',
loading: 0,
maxProgress: MAX_PROGRESS,
progressIncrease: PROGRESS_INCREASE,
showFastActions: false,
style: {},
updateTime: UPDATE_TIME,
scope: DEFAULT_SCOPE
}
constructor(props) {
super(props);
this.state = {
...initialState,
loading: LoadingBar.instances[props.scope] === undefined ? props.loading : LoadingBar.instances[props.scope].loading
};
this.style = {
opacity: '0',
transform: 'scaleX(0)',
transformOrigin: 'left',
width: '100%',
willChange: 'transform, opacity'
};
// Use default styling if there's no CSS class applied
if (!this.props.className) {
this.style.height = '3px';
this.style.backgroundColor = 'red';
this.style.position = 'absolute';
}
Object.assign(this.style, this.props.style);
this.boundSimulateProgress = this
.simulateProgress
.bind(this);
this.boundResetProgress = this
.resetProgress
.bind(this);
LoadingBar.instances[props.scope] = this;
}
componentDidMount() {
// Re-render the component after mount to fix problems with SSR and CSP.
//
// Apps that use Server Side Rendering and has Content Security Policy for style
// that doesn't allow inline styles should render an empty div and replace it
// with the actual Loading Bar after mount See:
// https://github.com/mironov/react-redux-loading-bar/issues/39
//
// eslint-disable-next-line react/no-did-mount-set-state
this.mounted = true;
if (this.state.loading > 0) {
this.launch();
}
}
handleUpdate(nextProps) {
if (this.shouldStart(nextProps)) {
this.launch();
} else if (this.shouldStop(nextProps)) {
if (this.state.percent === 0 && !this.props.showFastActions) {
// not even shown yet because the action finished quickly after start
clearInterval(this.state.progressInterval);
this.resetProgress();
} else {
// should progress to 100 percent
this.setState({percent: 100});
}
}
}
// componentWillReceiveProps(nextProps) { this.handleUpdate(nextProps); }
componentWillUnmount() {
clearInterval(this.state.progressInterval);
clearTimeout(this.state.terminatingAnimationTimeout);
}
shouldStart(nextProps) {
if (!this.state.loading && nextProps.loading > 0) {
this.setState({loading: nextProps.loading});
return true;
} else {
return false;
}
}
shouldStop(nextProps) {
if (this.state.progressInterval && nextProps.loading === 0) {
this.setState({loading: nextProps.loading});
return true;
} else {
return false;
}
}
shouldShow() {
return this.state.percent > 0 && this.state.percent <= 100;
}
launch() {
let {percent} = this.state;
const {terminatingAnimationTimeout} = this.state;
const loadingBarNotShown = !this._progressInterval;
const terminatingAnimationGoing = percent === 100;
if (loadingBarNotShown) {
this._progressInterval = setInterval(this.boundSimulateProgress, this.props.updateTime);
}
if (terminatingAnimationGoing) {
clearTimeout(terminatingAnimationTimeout);
}
percent = 0;
this.setState({progressInterval: this._progressInterval, percent: percent});
}
newPercent() {
const {percent} = this.state;
const {progressIncrease} = this.props;
// Use cos as a smoothing function Can be any function to slow down progress
// near the 100%
const smoothedProgressIncrease = (progressIncrease * Math.cos(percent * (Math.PI / 2 / 100)));
return percent + smoothedProgressIncrease;
}
simulateProgress() {
let {progressInterval, percent, terminatingAnimationTimeout} = this.state;
const {maxProgress} = this.props;
if (percent === 100) {
clearInterval(progressInterval);
terminatingAnimationTimeout = setTimeout(this.boundResetProgress, TERMINATING_ANIMATION_TIME);
progressInterval = null;
} else if (this.newPercent() <= maxProgress) {
percent = this.newPercent();
}
this.setState({percent, progressInterval, terminatingAnimationTimeout});
}
resetProgress() {
this.setState(initialState);
}
buildStyle() {
const animationTime = (this.state.percent !== 100 ? ANIMATION_TIME : TERMINATING_ANIMATION_TIME);
this.style = Object.assign({}, this.style, {
transform: `scaleX(${this.state.percent / 100})`,
transition: `transform ${animationTime}ms linear`,
opacity: this.shouldShow() ? '1' : '0'
});
return this.style;
}
render() {
// In order not to violate strict style CSP it's better to make an extra
// re-render after component mount
if (!this.mounted) {
return (<div />);
}
return (
<div>
<div style={this.buildStyle()} className={this.props.className} />
<div style={{
display: 'table',
clear: 'both'
}} />
</div>
);
}
}