Marquee.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. /*
  2. * <<
  3. * Davinci
  4. * ==
  5. * Copyright (C) 2016 - 2017 EDP
  6. * ==
  7. * Licensed under the Apache License, Version 2.0 (the "License");
  8. * you may not use this file except in compliance with the License.
  9. * You may obtain a copy of the License at
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing, software
  14. * distributed under the License is distributed on an "AS IS" BASIS,
  15. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. * See the License for the specific language governing permissions and
  17. * limitations under the License.
  18. * >>
  19. */
  20. import React from 'react'
  21. import classnames from 'classnames'
  22. import './Marquee.less'
  23. interface IMarqueeProps {
  24. className?: string
  25. loop?: boolean
  26. leading?: number
  27. trailing?: number
  28. fps?: number
  29. hoverToStop?: boolean
  30. }
  31. interface IMarqueeStates {
  32. animatedWidth: number
  33. overflowWidth: number
  34. }
  35. const ANIMATE_STEP = 1
  36. class Marquee extends React.Component<IMarqueeProps, IMarqueeStates> {
  37. private marqueeTimer: number
  38. private refBox = React.createRef<HTMLDivElement>()
  39. private refText = React.createRef<HTMLDivElement>()
  40. static defaultProps: Partial<IMarqueeProps> = {
  41. hoverToStop: true,
  42. loop: true,
  43. leading: 500,
  44. trailing: 800,
  45. fps: 50
  46. }
  47. state: IMarqueeStates = {
  48. animatedWidth: 0,
  49. overflowWidth: 0
  50. }
  51. public componentDidMount() {
  52. this.measureText()
  53. if (this.props.hoverToStop) {
  54. this.startAnimation()
  55. }
  56. }
  57. public componentDidUpdate() {
  58. this.measureText()
  59. if (this.props.hoverToStop && !this.marqueeTimer) {
  60. this.startAnimation()
  61. }
  62. }
  63. public componentWillUnmount() {
  64. window.clearTimeout(this.marqueeTimer)
  65. }
  66. private handleMouseEnter = () => {
  67. const { hoverToStop } = this.props
  68. if (hoverToStop) {
  69. window.clearTimeout(this.marqueeTimer)
  70. return
  71. }
  72. const { overflowWidth } = this.state
  73. if (overflowWidth > 0) {
  74. this.startAnimation()
  75. }
  76. }
  77. private handleMouseLeave = () => {
  78. const { hoverToStop } = this.props
  79. const { overflowWidth } = this.state
  80. if (hoverToStop && overflowWidth > 0) {
  81. this.startAnimation()
  82. return
  83. }
  84. window.clearTimeout(this.marqueeTimer)
  85. this.setState({ animatedWidth: 0 })
  86. }
  87. private startAnimation = () => {
  88. clearTimeout(this.marqueeTimer)
  89. const isLeading = this.state.animatedWidth === 0
  90. const TIMEOUT = (1 / this.props.fps) * 1000
  91. const timeout = isLeading ? this.props.leading : TIMEOUT
  92. const animate = () => {
  93. const { overflowWidth } = this.state
  94. let animatedWidth = this.state.animatedWidth + ANIMATE_STEP
  95. const isRoundOver = animatedWidth > overflowWidth
  96. if (isRoundOver) {
  97. if (!this.props.loop) {
  98. return
  99. }
  100. animatedWidth = 0
  101. }
  102. if (isRoundOver && this.props.trailing) {
  103. this.marqueeTimer = window.setTimeout(() => {
  104. this.setState({ animatedWidth })
  105. this.marqueeTimer = window.setTimeout(animate, TIMEOUT)
  106. }, this.props.trailing)
  107. } else {
  108. this.setState({ animatedWidth })
  109. this.marqueeTimer = window.setTimeout(animate, TIMEOUT)
  110. }
  111. }
  112. this.marqueeTimer = window.setTimeout(animate, timeout)
  113. }
  114. private measureText = () => {
  115. if (!this.refBox.current || !this.refText.current) {
  116. return
  117. }
  118. const boxWidth = this.refBox.current.offsetWidth
  119. const textWidth = this.refText.current.offsetWidth
  120. const overflowWidth = textWidth - boxWidth
  121. if (overflowWidth !== this.state.overflowWidth) {
  122. this.setState({ overflowWidth })
  123. }
  124. }
  125. public render() {
  126. const { className } = this.props
  127. const { overflowWidth, animatedWidth } = this.state
  128. const cls = classnames({
  129. 'marquee-box': true,
  130. [className]: !!className
  131. })
  132. const boxProps =
  133. overflowWidth < 0
  134. ? {}
  135. : {
  136. onMouseEnter: this.handleMouseEnter,
  137. onMouseLeave: this.handleMouseLeave
  138. }
  139. return (
  140. <div ref={this.refBox} className={cls} {...boxProps}>
  141. <div
  142. ref={this.refText}
  143. className="marquee-text"
  144. style={{ right: animatedWidth }}
  145. >
  146. {this.props.children}
  147. </div>
  148. </div>
  149. )
  150. }
  151. }
  152. export default Marquee
  153. // refs:
  154. // 1. https://github.com/jasonslyvia/react-marquee/blob/master/src/index.js
  155. // 2. https://github.com/ant-design/ant-design-mobile/blob/master/components/notice-bar/Marquee.tsx