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 PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import Measure from './Measure';
|
||||||
|
import styles from './Marquee.css';
|
||||||
|
|
||||||
const FPS = 20;
|
const SPEED = 50; // pixels per second
|
||||||
const STEP = 1;
|
|
||||||
const TIMEOUT = 1 / FPS * 1000;
|
|
||||||
|
|
||||||
class Marquee extends Component {
|
class Marquee extends Component {
|
||||||
|
|
||||||
static propTypes = {
|
//
|
||||||
text: PropTypes.string,
|
// Lifecycle
|
||||||
title: PropTypes.string,
|
|
||||||
hoverToStop: PropTypes.bool,
|
constructor(props) {
|
||||||
loop: PropTypes.bool,
|
super(props);
|
||||||
className: PropTypes.string
|
|
||||||
};
|
this.state = {
|
||||||
|
containerWidth: 0,
|
||||||
static defaultProps = {
|
overflowWidth: 0,
|
||||||
text: '',
|
animationState: null,
|
||||||
title: '',
|
key: 0
|
||||||
hoverToStop: true,
|
};
|
||||||
loop: false
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
animatedWidth: 0,
|
|
||||||
overflowWidth: 0,
|
|
||||||
direction: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.measureText();
|
|
||||||
|
|
||||||
if (this.props.hoverToStop) {
|
|
||||||
this.startAnimation();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (this.props.text.length !== nextProps.text.length) {
|
if (this.props.text !== prevProps.text) {
|
||||||
clearTimeout(this.marqueeTimer);
|
// reset the component, set a new key to force re-render so new text isn't in old position
|
||||||
this.setState({ animatedWidth: 0, direction: 0 });
|
this.setState({
|
||||||
|
overflowWidth: 0,
|
||||||
|
animationState: null,
|
||||||
|
key: this.state.key + 1
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
const containerWidth = this.state.containerWidth;
|
||||||
this.measureText();
|
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.setState({ overflowWidth }, () => {
|
||||||
this.startAnimation();
|
if (triggerUpdate) {
|
||||||
|
this.onHandleMouseEnter();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
//
|
||||||
clearTimeout(this.marqueeTimer);
|
// Listeners
|
||||||
}
|
|
||||||
|
|
||||||
onHandleMouseEnter = () => {
|
onHandleMouseEnter = () => {
|
||||||
if (this.props.hoverToStop) {
|
const {
|
||||||
clearTimeout(this.marqueeTimer);
|
animationState,
|
||||||
} else if (this.state.overflowWidth > 0) {
|
overflowWidth
|
||||||
this.startAnimation();
|
} = this.state;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onHandleMouseLeave = () => {
|
if (animationState === null && overflowWidth > 0) {
|
||||||
if (this.props.hoverToStop && this.state.overflowWidth > 0) {
|
this.setState({ animationState: 'toLeft' });
|
||||||
this.startAnimation();
|
|
||||||
} else {
|
|
||||||
clearTimeout(this.marqueeTimer);
|
|
||||||
this.setState({ animatedWidth: 0 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startAnimation = () => {
|
onTransitionEnd = (payload) => {
|
||||||
clearTimeout(this.marqueeTimer);
|
const {
|
||||||
const isLeading = this.state.animatedWidth === 0;
|
animationState
|
||||||
const timeout = isLeading ? 0 : TIMEOUT;
|
} = this.state;
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
if (endOfText) {
|
if (animationState === 'toLeft') {
|
||||||
direction = direction === 1;
|
this.setState({ animationState: 'toRight' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRoundOver) {
|
|
||||||
if (this.props.loop) {
|
|
||||||
direction = direction === 0;
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ animatedWidth, direction });
|
if (animationState === 'toRight') {
|
||||||
this.marqueeTimer = setTimeout(animate, TIMEOUT);
|
this.setState({ animationState: null });
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.marqueeTimer = setTimeout(animate, timeout);
|
onContainerMeasure = ({ width }) => {
|
||||||
|
this.setState({ containerWidth: width });
|
||||||
}
|
}
|
||||||
|
|
||||||
measureText = () => {
|
//
|
||||||
const container = this.container;
|
// Render
|
||||||
const node = this.text;
|
|
||||||
|
|
||||||
if (container && node) {
|
render() {
|
||||||
const containerWidth = container.offsetWidth;
|
const {
|
||||||
const textWidth = node.offsetWidth;
|
text
|
||||||
const overflowWidth = textWidth - containerWidth;
|
} = this.props;
|
||||||
|
|
||||||
if (overflowWidth !== this.state.overflowWidth) {
|
const {
|
||||||
this.setState({ overflowWidth });
|
key,
|
||||||
}
|
overflowWidth,
|
||||||
}
|
animationState
|
||||||
}
|
} = this.state;
|
||||||
|
|
||||||
|
const moveDist = -overflowWidth - 10;
|
||||||
|
const duration = -moveDist / SPEED;
|
||||||
|
|
||||||
render() {
|
|
||||||
const style = {
|
const style = {
|
||||||
position: 'relative',
|
'--duration': `${duration}s`,
|
||||||
right: this.state.animatedWidth,
|
'--distance': `${moveDist}px`
|
||||||
whiteSpace: 'nowrap'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.state.overflowWidth < 0) {
|
return (
|
||||||
return (
|
<Measure
|
||||||
|
key={key}
|
||||||
|
className={styles.container}
|
||||||
|
onMeasure={this.onContainerMeasure}
|
||||||
|
onMouseEnter={this.onHandleMouseEnter}
|
||||||
|
onTouchStart={this.onHandleMouseEnter}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
ref={(el) => {
|
className={classNames(
|
||||||
this.container = el;
|
styles.inner,
|
||||||
}}
|
animationState === 'toLeft' && styles.toLeft
|
||||||
className={`ui-marquee ${this.props.className}`}
|
)}
|
||||||
style={{ overflow: 'hidden' }}
|
style={style}
|
||||||
|
onTransitionEnd={this.onTransitionEnd}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
this.text = el;
|
this.text = el;
|
||||||
}}
|
}}
|
||||||
style={style}
|
title={text}
|
||||||
title={(this.props.title && (this.props.text !== this.props.title)) ? `Original Title: ${this.props.title}` : this.props.text}
|
|
||||||
>
|
>
|
||||||
{this.props.text}
|
{text}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
</Measure>
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Marquee.propTypes = {
|
||||||
|
text: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
export default Marquee;
|
export default Marquee;
|
||||||
|
Loading…
Reference in new issue