Rework marquee

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

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

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

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

@ -158,7 +158,7 @@ class BookDetails extends Component {
overviewHeight
} = this.state;
const marqueeWidth = (titleWidth - 170);
const marqueeWidth = (titleWidth - 165);
return (
<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 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…
Cancel
Save