Rework marquee

This commit is contained in:
ta264 2021-04-11 19:14:41 +01:00
parent 5714f1c913
commit f884a2689a
6 changed files with 125 additions and 147 deletions

View file

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

View file

@ -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';

View file

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

View file

@ -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}>

View file

@ -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));
}

View file

@ -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,
loop: PropTypes.bool,
className: PropTypes.string
};
static defaultProps = { constructor(props) {
text: '', super(props);
title: '',
hoverToStop: true,
loop: false
};
state = { this.state = {
animatedWidth: 0, containerWidth: 0,
overflowWidth: 0, overflowWidth: 0,
direction: 0 animationState: null,
}; key: 0
componentDidMount() {
this.measureText();
if (this.props.hoverToStop) {
this.startAnimation();
}
}
componentWillReceiveProps(nextProps) {
if (this.props.text.length !== nextProps.text.length) {
clearTimeout(this.marqueeTimer);
this.setState({ animatedWidth: 0, direction: 0 });
}
}
componentDidUpdate() {
this.measureText();
if (this.props.hoverToStop) {
this.startAnimation();
}
}
componentWillUnmount() {
clearTimeout(this.marqueeTimer);
}
onHandleMouseEnter = () => {
if (this.props.hoverToStop) {
clearTimeout(this.marqueeTimer);
} else if (this.state.overflowWidth > 0) {
this.startAnimation();
}
}
onHandleMouseLeave = () => {
if (this.props.hoverToStop && this.state.overflowWidth > 0) {
this.startAnimation();
} else {
clearTimeout(this.marqueeTimer);
this.setState({ animatedWidth: 0 });
}
}
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;
if (endOfText) {
direction = direction === 1;
}
if (isRoundOver) {
if (this.props.loop) {
direction = direction === 0;
} else {
return;
}
}
this.setState({ animatedWidth, direction });
this.marqueeTimer = setTimeout(animate, TIMEOUT);
}; };
this.marqueeTimer = setTimeout(animate, timeout);
} }
measureText = () => { componentDidUpdate(prevProps) {
const container = this.container; 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;
}
const containerWidth = this.state.containerWidth;
const node = this.text; const node = this.text;
if (container && node) { if (containerWidth && node) {
const containerWidth = container.offsetWidth;
const textWidth = node.offsetWidth; const textWidth = node.offsetWidth;
const overflowWidth = textWidth - containerWidth; // eslint-disable-next-line no-bitwise
const overflowWidth = (textWidth - containerWidth + 10) | 0; // 10 margin, round towards 0
if (overflowWidth !== this.state.overflowWidth) { if (overflowWidth !== this.state.overflowWidth) {
this.setState({ overflowWidth }); const triggerUpdate = overflowWidth > 0 && this.state.overflowWidth === 0;
this.setState({ overflowWidth }, () => {
if (triggerUpdate) {
this.onHandleMouseEnter();
}
});
} }
} }
} }
//
// Listeners
onHandleMouseEnter = () => {
const {
animationState,
overflowWidth
} = this.state;
if (animationState === null && overflowWidth > 0) {
this.setState({ animationState: 'toLeft' });
}
}
onTransitionEnd = (payload) => {
const {
animationState
} = this.state;
if (animationState === 'toLeft') {
this.setState({ animationState: 'toRight' });
}
if (animationState === 'toRight') {
this.setState({ animationState: null });
}
}
onContainerMeasure = ({ width }) => {
this.setState({ containerWidth: width });
}
//
// Render
render() { render() {
const {
text
} = this.props;
const {
key,
overflowWidth,
animationState
} = this.state;
const moveDist = -overflowWidth - 10;
const duration = -moveDist / SPEED;
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;