parent
5714f1c913
commit
f884a2689a
@ -0,0 +1,17 @@
|
||||
.container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding-left: 10px;
|
||||
white-space: nowrap;
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
-webkit-mask-image: linear-gradient(to right, transparent, $white 10px, $white 90%, transparent), linear-gradient(to left, transparent, $white 10px, $white 90%, transparent);
|
||||
mask-image: linear-gradient(to right, transparent, $white 10px, $white 90%, transparent), linear-gradient(to left, transparent, $white 10px, $white 90%, transparent);
|
||||
}
|
||||
|
||||
.inner {
|
||||
transition: transform var(--duration) ease-in-out;
|
||||
}
|
||||
|
||||
.toLeft {
|
||||
transform: translateX(var(--distance));
|
||||
}
|
@ -1,181 +1,144 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Measure from './Measure';
|
||||
import styles from './Marquee.css';
|
||||
|
||||
const FPS = 20;
|
||||
const STEP = 1;
|
||||
const TIMEOUT = 1 / FPS * 1000;
|
||||
const SPEED = 50; // pixels per second
|
||||
|
||||
class Marquee extends Component {
|
||||
|
||||
static propTypes = {
|
||||
text: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
hoverToStop: PropTypes.bool,
|
||||
loop: PropTypes.bool,
|
||||
className: PropTypes.string
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
text: '',
|
||||
title: '',
|
||||
hoverToStop: true,
|
||||
loop: false
|
||||
};
|
||||
|
||||
state = {
|
||||
animatedWidth: 0,
|
||||
overflowWidth: 0,
|
||||
direction: 0
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.measureText();
|
||||
|
||||
if (this.props.hoverToStop) {
|
||||
this.startAnimation();
|
||||
}
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
containerWidth: 0,
|
||||
overflowWidth: 0,
|
||||
animationState: null,
|
||||
key: 0
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.text.length !== nextProps.text.length) {
|
||||
clearTimeout(this.marqueeTimer);
|
||||
this.setState({ animatedWidth: 0, direction: 0 });
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.text !== prevProps.text) {
|
||||
// reset the component, set a new key to force re-render so new text isn't in old position
|
||||
this.setState({
|
||||
overflowWidth: 0,
|
||||
animationState: null,
|
||||
key: this.state.key + 1
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.measureText();
|
||||
const containerWidth = this.state.containerWidth;
|
||||
const node = this.text;
|
||||
|
||||
if (containerWidth && node) {
|
||||
const textWidth = node.offsetWidth;
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const overflowWidth = (textWidth - containerWidth + 10) | 0; // 10 margin, round towards 0
|
||||
|
||||
if (overflowWidth !== this.state.overflowWidth) {
|
||||
const triggerUpdate = overflowWidth > 0 && this.state.overflowWidth === 0;
|
||||
|
||||
if (this.props.hoverToStop) {
|
||||
this.startAnimation();
|
||||
this.setState({ overflowWidth }, () => {
|
||||
if (triggerUpdate) {
|
||||
this.onHandleMouseEnter();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.marqueeTimer);
|
||||
}
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onHandleMouseEnter = () => {
|
||||
if (this.props.hoverToStop) {
|
||||
clearTimeout(this.marqueeTimer);
|
||||
} else if (this.state.overflowWidth > 0) {
|
||||
this.startAnimation();
|
||||
}
|
||||
}
|
||||
const {
|
||||
animationState,
|
||||
overflowWidth
|
||||
} = this.state;
|
||||
|
||||
onHandleMouseLeave = () => {
|
||||
if (this.props.hoverToStop && this.state.overflowWidth > 0) {
|
||||
this.startAnimation();
|
||||
} else {
|
||||
clearTimeout(this.marqueeTimer);
|
||||
this.setState({ animatedWidth: 0 });
|
||||
if (animationState === null && overflowWidth > 0) {
|
||||
this.setState({ animationState: 'toLeft' });
|
||||
}
|
||||
}
|
||||
|
||||
startAnimation = () => {
|
||||
clearTimeout(this.marqueeTimer);
|
||||
const isLeading = this.state.animatedWidth === 0;
|
||||
const timeout = isLeading ? 0 : TIMEOUT;
|
||||
|
||||
const animate = () => {
|
||||
const { overflowWidth } = this.state;
|
||||
let animatedWidth = this.state.animatedWidth;
|
||||
let direction = this.state.direction;
|
||||
|
||||
if (direction === 0) {
|
||||
animatedWidth = this.state.animatedWidth + STEP;
|
||||
} else {
|
||||
animatedWidth = this.state.animatedWidth - STEP;
|
||||
}
|
||||
|
||||
const isRoundOver = animatedWidth < 0;
|
||||
const endOfText = animatedWidth > overflowWidth;
|
||||
onTransitionEnd = (payload) => {
|
||||
const {
|
||||
animationState
|
||||
} = this.state;
|
||||
|
||||
if (endOfText) {
|
||||
direction = direction === 1;
|
||||
}
|
||||
|
||||
if (isRoundOver) {
|
||||
if (this.props.loop) {
|
||||
direction = direction === 0;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (animationState === 'toLeft') {
|
||||
this.setState({ animationState: 'toRight' });
|
||||
}
|
||||
|
||||
this.setState({ animatedWidth, direction });
|
||||
this.marqueeTimer = setTimeout(animate, TIMEOUT);
|
||||
};
|
||||
if (animationState === 'toRight') {
|
||||
this.setState({ animationState: null });
|
||||
}
|
||||
}
|
||||
|
||||
this.marqueeTimer = setTimeout(animate, timeout);
|
||||
onContainerMeasure = ({ width }) => {
|
||||
this.setState({ containerWidth: width });
|
||||
}
|
||||
|
||||
measureText = () => {
|
||||
const container = this.container;
|
||||
const node = this.text;
|
||||
//
|
||||
// Render
|
||||
|
||||
if (container && node) {
|
||||
const containerWidth = container.offsetWidth;
|
||||
const textWidth = node.offsetWidth;
|
||||
const overflowWidth = textWidth - containerWidth;
|
||||
render() {
|
||||
const {
|
||||
text
|
||||
} = this.props;
|
||||
|
||||
if (overflowWidth !== this.state.overflowWidth) {
|
||||
this.setState({ overflowWidth });
|
||||
}
|
||||
}
|
||||
}
|
||||
const {
|
||||
key,
|
||||
overflowWidth,
|
||||
animationState
|
||||
} = this.state;
|
||||
|
||||
const moveDist = -overflowWidth - 10;
|
||||
const duration = -moveDist / SPEED;
|
||||
|
||||
render() {
|
||||
const style = {
|
||||
position: 'relative',
|
||||
right: this.state.animatedWidth,
|
||||
whiteSpace: 'nowrap'
|
||||
'--duration': `${duration}s`,
|
||||
'--distance': `${moveDist}px`
|
||||
};
|
||||
|
||||
if (this.state.overflowWidth < 0) {
|
||||
return (
|
||||
return (
|
||||
<Measure
|
||||
key={key}
|
||||
className={styles.container}
|
||||
onMeasure={this.onContainerMeasure}
|
||||
onMouseEnter={this.onHandleMouseEnter}
|
||||
onTouchStart={this.onHandleMouseEnter}
|
||||
>
|
||||
<div
|
||||
ref={(el) => {
|
||||
this.container = el;
|
||||
}}
|
||||
className={`ui-marquee ${this.props.className}`}
|
||||
style={{ overflow: 'hidden' }}
|
||||
className={classNames(
|
||||
styles.inner,
|
||||
animationState === 'toLeft' && styles.toLeft
|
||||
)}
|
||||
style={style}
|
||||
onTransitionEnd={this.onTransitionEnd}
|
||||
>
|
||||
<span
|
||||
ref={(el) => {
|
||||
this.text = el;
|
||||
}}
|
||||
style={style}
|
||||
title={(this.props.title && (this.props.text !== this.props.title)) ? `Original Title: ${this.props.title}` : this.props.text}
|
||||
title={text}
|
||||
>
|
||||
{this.props.text}
|
||||
{text}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={(el) => {
|
||||
this.container = el;
|
||||
}}
|
||||
className={`ui-marquee ${this.props.className}`.trim()}
|
||||
style={{ overflow: 'hidden' }}
|
||||
onMouseEnter={this.onHandleMouseEnter}
|
||||
onMouseLeave={this.onHandleMouseLeave}
|
||||
>
|
||||
<span
|
||||
ref={(el) => {
|
||||
this.text = el;
|
||||
}}
|
||||
style={style}
|
||||
title={(this.props.title && (this.props.text !== this.props.title)) ? `Original Title: ${this.props.title}` : this.props.text}
|
||||
>
|
||||
{this.props.text}
|
||||
</span>
|
||||
</div>
|
||||
</Measure>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Marquee.propTypes = {
|
||||
text: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default Marquee;
|
||||
|
Loading…
Reference in new issue