Rework marquee

pull/965/head
ta264 4 years ago
parent 5714f1c913
commit f884a2689a

@ -78,7 +78,6 @@
.toggleMonitoredContainer { .toggleMonitoredContainer {
align-self: center; align-self: center;
margin-right: 10px;
} }
.monitorToggleButton { .monitorToggleButton {

@ -223,7 +223,7 @@ class AuthorDetails extends Component {
overviewHeight overviewHeight
} = this.state; } = this.state;
const marqueeWidth = (titleWidth - 170); const marqueeWidth = (titleWidth - 165);
const continuing = status === 'continuing'; const continuing = status === 'continuing';

@ -65,7 +65,6 @@
.toggleMonitoredContainer { .toggleMonitoredContainer {
align-self: center; align-self: center;
margin-right: 10px;
} }
.monitorToggleButton { .monitorToggleButton {

@ -158,7 +158,7 @@ class BookDetails extends Component {
overviewHeight overviewHeight
} = this.state; } = this.state;
const marqueeWidth = (titleWidth - 170); const marqueeWidth = (titleWidth - 165);
return ( return (
<PageContent title={title}> <PageContent title={title}>

@ -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…
Cancel
Save