index.jsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. /*
  2. Copyright (C) 2017 Cloudbase Solutions SRL
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. */
  14. // @flow
  15. import React from 'react'
  16. import ReactDOM from 'react-dom'
  17. import { observer } from 'mobx-react'
  18. import styled, { injectGlobal } from 'styled-components'
  19. import Datetime from 'react-datetime'
  20. import moment from 'moment'
  21. import DropdownButton from '../../atoms/DropdownButton'
  22. import DomUtils from '../../../utils/DomUtils'
  23. import DateUtils from '../../../utils/DateUtils'
  24. import StyleProps from '../../styleUtils/StyleProps'
  25. import style from './style.js'
  26. require('moment/locale/en-gb')
  27. injectGlobal`${style}`
  28. const Wrapper = styled.div`
  29. width: ${StyleProps.inputSizes.regular.width}px;
  30. `
  31. const DropdownButtonStyled = styled(DropdownButton)`
  32. font-size: 12px;
  33. `
  34. const Portal = styled.div`
  35. position: absolute;
  36. z-index: 10;
  37. &.hideTip {
  38. .rdtPicker:after {
  39. content: none;
  40. }
  41. }
  42. `
  43. const DatetimeStyled = styled(Datetime)``
  44. type Props = {
  45. value: ?Date,
  46. onChange: (date: Date) => void,
  47. isValidDate?: (currentDate: Date, selectedDate: Date) => boolean,
  48. timezone: 'utc' | 'local',
  49. useBold?: boolean,
  50. }
  51. type State = {
  52. showPicker: boolean,
  53. date: ?moment$Moment,
  54. }
  55. @observer
  56. class DatetimePicker extends React.Component<Props, State> {
  57. itemMouseDown: boolean
  58. portalRef: HTMLElement
  59. buttonRef: HTMLElement
  60. scrollableParent: HTMLElement
  61. constructor() {
  62. super()
  63. this.state = {
  64. showPicker: false,
  65. date: null,
  66. }
  67. // $FlowIssue
  68. this.handlePageClick = this.handlePageClick.bind(this)
  69. // $FlowIssue
  70. this.handleScroll = this.handleScroll.bind(this)
  71. }
  72. componentWillMount() {
  73. if (this.props.value) {
  74. this.setState({ date: moment(this.props.value) })
  75. }
  76. }
  77. componentDidMount() {
  78. window.addEventListener('mousedown', this.handlePageClick, false)
  79. if (this.buttonRef) {
  80. this.scrollableParent = DomUtils.getScrollableParent(this.buttonRef)
  81. this.scrollableParent.addEventListener('scroll', this.handleScroll)
  82. }
  83. }
  84. componentDidUpdate() {
  85. this.setPortalPosition()
  86. }
  87. componentWillUnmount() {
  88. window.removeEventListener('mousedown', this.handlePageClick, false)
  89. this.scrollableParent.removeEventListener('scroll', this.handleScroll, false)
  90. }
  91. setPortalPosition() {
  92. if (!this.portalRef || !this.buttonRef) {
  93. return
  94. }
  95. const buttonRect = this.buttonRef.getBoundingClientRect()
  96. const leftOffset = (buttonRect.left - (this.portalRef.offsetWidth - buttonRect.width)) + 10
  97. const tipHeight = 12
  98. let topOffset = buttonRect.top + this.buttonRef.offsetHeight + tipHeight
  99. let listHeight = this.portalRef.offsetHeight
  100. if (topOffset + listHeight > window.innerHeight) {
  101. topOffset = window.innerHeight - listHeight - 16
  102. this.portalRef.classList.add('hideTip')
  103. } else {
  104. this.portalRef.classList.remove('hideTip')
  105. }
  106. this.portalRef.style.top = `${topOffset + window.pageYOffset}px`
  107. this.portalRef.style.left = `${leftOffset + window.pageXOffset}px`
  108. }
  109. isValidDate(currentDate: Date, selectedDate: Date): boolean {
  110. if (!this.props.isValidDate) {
  111. return true
  112. }
  113. return this.props.isValidDate(currentDate, selectedDate)
  114. }
  115. handleScroll() {
  116. if (this.buttonRef) {
  117. if (DomUtils.isElementInViewport(this.buttonRef, this.scrollableParent)) {
  118. this.setPortalPosition()
  119. } else if (this.state.showPicker) {
  120. this.setState({ showPicker: false })
  121. }
  122. }
  123. }
  124. handlePageClick(e: Event) {
  125. let path = DomUtils.getEventPath(e)
  126. if (!this.itemMouseDown && !path.find(n => n.className === 'rdtPicker')) {
  127. this.dispatchChange()
  128. this.setState({ showPicker: false })
  129. }
  130. }
  131. handleDropdownClick() {
  132. this.dispatchChange()
  133. this.setState({ showPicker: !this.state.showPicker })
  134. }
  135. handleChange(newDate: Date) {
  136. let date = moment(newDate)
  137. if (this.props.timezone === 'utc') {
  138. date = DateUtils.getLocalTime(newDate)
  139. }
  140. this.setState({ date })
  141. }
  142. dispatchChange() {
  143. if (
  144. this.state.date
  145. && this.state.showPicker
  146. && this.state.date.toDate().getTime() !== (this.props.value && this.props.value.getTime())
  147. ) {
  148. this.props.onChange(this.state.date.toDate())
  149. }
  150. }
  151. renderDateTimePicker(timezoneDate: ?moment$Moment) {
  152. if (!this.state.showPicker) {
  153. return null
  154. }
  155. let body: any = document.body
  156. return ReactDOM.createPortal((
  157. <Portal innerRef={e => { this.portalRef = e }}>
  158. <DatetimeStyled
  159. input={false}
  160. value={timezoneDate}
  161. style={{ top: 0, right: 0 }}
  162. onChange={date => { this.handleChange(date) }}
  163. dateFormat="DD/MM/YYYY"
  164. timeFormat="hh:mm A"
  165. locale="en-gb"
  166. isValidDate={(currentDate, selectedDate) => this.isValidDate(currentDate, selectedDate)}
  167. />
  168. </Portal>
  169. ), body)
  170. }
  171. render() {
  172. let timezoneDate = this.state.date
  173. if (this.props.timezone === 'utc' && timezoneDate) {
  174. timezoneDate = DateUtils.getUtcTime(timezoneDate)
  175. }
  176. return (
  177. <Wrapper>
  178. <DropdownButtonStyled
  179. customRef={e => { this.buttonRef = e }}
  180. data-test-id="dropdownButton"
  181. width={207}
  182. value={(timezoneDate && moment(timezoneDate).format('DD/MM/YYYY hh:mm A')) || '-'}
  183. centered
  184. useBold={this.props.useBold}
  185. onClick={() => { this.handleDropdownClick() }}
  186. onMouseDown={() => { this.itemMouseDown = true }}
  187. onMouseUp={() => { this.itemMouseDown = false }}
  188. />
  189. {this.renderDateTimePicker(timezoneDate)}
  190. </Wrapper>
  191. )
  192. }
  193. }
  194. export default DatetimePicker