diff --git a/frontend/src/Book/Details/BookDetails.js b/frontend/src/Book/Details/BookDetails.js index 03b021d32..4b6094916 100644 --- a/frontend/src/Book/Details/BookDetails.js +++ b/frontend/src/Book/Details/BookDetails.js @@ -14,6 +14,8 @@ import Icon from 'Components/Icon'; import Label from 'Components/Label'; import IconButton from 'Components/Link/IconButton'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Marquee from 'Components/Marquee'; +import Measure from 'Components/Measure'; import MonitorToggleButton from 'Components/MonitorToggleButton'; import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; @@ -57,7 +59,8 @@ class BookDetails extends Component { isRetagModalOpen: false, isEditBookModalOpen: false, isDeleteBookModalOpen: false, - selectedTabIndex: 0 + selectedTabIndex: 0, + titleWidth: 0 }; } @@ -103,6 +106,10 @@ class BookDetails extends Component { this.setState({ selectedTabIndex: index }); } + onTitleMeasure = ({ width }) => { + this.setState({ titleWidth: width }); + } + // // Render @@ -141,9 +148,12 @@ class BookDetails extends Component { isRetagModalOpen, isEditBookModalOpen, isDeleteBookModalOpen, - selectedTabIndex + selectedTabIndex, + titleWidth } = this.state; + const marqueeWidth = (titleWidth - 150); + return ( @@ -217,51 +227,53 @@ class BookDetails extends Component { />
-
-
- -
- -
+ +
+
+ +
+ +
-
- {title} -
+
+ + - + -
- +
+ - + - + +
- +
@@ -497,6 +509,7 @@ BookDetails.propTypes = { author: PropTypes.object, previousBook: PropTypes.object, nextBook: PropTypes.object, + isSmallScreen: PropTypes.bool.isRequired, onMonitorTogglePress: PropTypes.func.isRequired, onRefreshPress: PropTypes.func, onSearchPress: PropTypes.func.isRequired diff --git a/frontend/src/Components/Marquee.js b/frontend/src/Components/Marquee.js new file mode 100644 index 000000000..900ccafd6 --- /dev/null +++ b/frontend/src/Components/Marquee.js @@ -0,0 +1,181 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; + +const FPS = 20; +const STEP = 1; +const TIMEOUT = 1 / FPS * 1000; + +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(); + } + } + + 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 = () => { + const container = this.container; + const node = this.text; + + if (container && node) { + const containerWidth = container.offsetWidth; + const textWidth = node.offsetWidth; + const overflowWidth = textWidth - containerWidth; + + if (overflowWidth !== this.state.overflowWidth) { + this.setState({ overflowWidth }); + } + } + } + + render() { + const style = { + position: 'relative', + right: this.state.animatedWidth, + whiteSpace: 'nowrap' + }; + + if (this.state.overflowWidth < 0) { + return ( +
{ + this.container = el; + }} + className={`ui-marquee ${this.props.className}`} + style={{ overflow: 'hidden' }} + > + { + 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} + +
+ ); + } + + return ( +
{ + this.container = el; + }} + className={`ui-marquee ${this.props.className}`.trim()} + style={{ overflow: 'hidden' }} + onMouseEnter={this.onHandleMouseEnter} + onMouseLeave={this.onHandleMouseLeave} + > + { + 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} + +
+ ); + } +} + +export default Marquee;