DatetimePicker.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  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. import autobind from "autobind-decorator";
  15. import { DateTime } from "luxon";
  16. import { observer } from "mobx-react";
  17. import React from "react";
  18. import Datetime from "react-datetime";
  19. import ReactDOM from "react-dom";
  20. import styled, { createGlobalStyle } from "styled-components";
  21. import { ThemeProps } from "@src/components/Theme";
  22. import DropdownButton from "@src/components/ui/Dropdowns/DropdownButton";
  23. import DateUtils from "@src/utils/DateUtils";
  24. import DomUtils from "@src/utils/DomUtils";
  25. import style from "./style";
  26. const GlobalStyle = createGlobalStyle`${style}`;
  27. const Wrapper = styled.div<any>`
  28. width: ${ThemeProps.inputSizes.regular.width}px;
  29. `;
  30. const DropdownButtonStyled = styled(DropdownButton)`
  31. font-size: 12px;
  32. `;
  33. const Portal = styled.div<any>`
  34. position: absolute;
  35. z-index: 10;
  36. &.hideTip {
  37. .rdtPicker:after {
  38. content: none;
  39. }
  40. }
  41. `;
  42. const DatetimeStyled = styled(Datetime)<any>`
  43. ${ThemeProps.boxShadow}
  44. `;
  45. type Props = {
  46. value: Date | null;
  47. onChange: (date: Date) => void;
  48. isValidDate?: (currentDate: Date, selectedDate?: Date) => boolean;
  49. timezone: "utc" | "local";
  50. useBold?: boolean;
  51. dispatchChangeContinously?: boolean;
  52. };
  53. type State = {
  54. showPicker: boolean;
  55. date: DateTime | null;
  56. };
  57. @observer
  58. class DatetimePicker extends React.Component<Props, State> {
  59. state: State = {
  60. showPicker: false,
  61. date: null,
  62. };
  63. itemMouseDown: boolean | undefined;
  64. portalRef: HTMLElement | undefined | null;
  65. buttonRef: HTMLElement | undefined | null;
  66. scrollableParent: HTMLElement | undefined | null;
  67. UNSAFE_componentWillMount() {
  68. if (this.props.value) {
  69. this.setState({
  70. date: DateUtils.getLocalDate(this.props.value),
  71. });
  72. }
  73. }
  74. componentDidMount() {
  75. window.addEventListener("mousedown", this.handlePageClick, false);
  76. if (this.buttonRef) {
  77. this.scrollableParent = DomUtils.getScrollableParent(this.buttonRef);
  78. this.scrollableParent.addEventListener("scroll", this.handleScroll);
  79. }
  80. }
  81. UNSAFE_componentWillReceiveProps(newProps: Props) {
  82. if (newProps.value?.getTime() !== this.props.value?.getTime()) {
  83. this.setState({
  84. date: newProps.value && DateUtils.getLocalDate(newProps.value),
  85. });
  86. }
  87. }
  88. componentDidUpdate() {
  89. this.setPortalPosition();
  90. }
  91. componentWillUnmount() {
  92. window.removeEventListener("mousedown", this.handlePageClick, false);
  93. if (this.scrollableParent) {
  94. this.scrollableParent.removeEventListener(
  95. "scroll",
  96. this.handleScroll,
  97. false
  98. );
  99. }
  100. }
  101. setPortalPosition() {
  102. if (!this.portalRef || !this.buttonRef) {
  103. return;
  104. }
  105. const buttonRect = this.buttonRef.getBoundingClientRect();
  106. const leftOffset =
  107. buttonRect.left - (this.portalRef.offsetWidth - buttonRect.width) + 10;
  108. const tipHeight = 12;
  109. let topOffset = buttonRect.top + this.buttonRef.offsetHeight + tipHeight;
  110. const listHeight = this.portalRef.offsetHeight;
  111. if (topOffset + listHeight > window.innerHeight) {
  112. topOffset = window.innerHeight - listHeight - 16;
  113. this.portalRef.classList.add("hideTip");
  114. } else {
  115. this.portalRef.classList.remove("hideTip");
  116. }
  117. this.portalRef.style.top = `${topOffset + window.pageYOffset}px`;
  118. this.portalRef.style.left = `${leftOffset + window.pageXOffset}px`;
  119. }
  120. isValidDate(currentDate: Date, selectedDate?: Date): boolean {
  121. if (!this.props.isValidDate) {
  122. return true;
  123. }
  124. return this.props.isValidDate(currentDate, selectedDate);
  125. }
  126. @autobind
  127. handleScroll() {
  128. if (this.buttonRef) {
  129. if (DomUtils.isElementInViewport(this.buttonRef, this.scrollableParent)) {
  130. this.setPortalPosition();
  131. } else if (this.state.showPicker) {
  132. this.setState({ showPicker: false });
  133. }
  134. }
  135. }
  136. @autobind
  137. handlePageClick(e: Event) {
  138. const path = DomUtils.getEventPath(e);
  139. if (!this.itemMouseDown && !path.find(n => n.className === "rdtPicker")) {
  140. this.dispatchChange();
  141. this.setState({ showPicker: false });
  142. }
  143. }
  144. handleDropdownClick() {
  145. this.dispatchChange();
  146. this.setState(prevState => ({ showPicker: !prevState.showPicker }));
  147. }
  148. handleChange(newDate: Date) {
  149. let date = DateUtils.getLocalDate(newDate);
  150. if (this.props.timezone === "utc") {
  151. date = date.setZone("utc");
  152. }
  153. this.setState({ date }, () => {
  154. if (this.props.dispatchChangeContinously) {
  155. this.dispatchChange();
  156. }
  157. });
  158. }
  159. dispatchChange() {
  160. if (
  161. this.state.date &&
  162. this.state.showPicker &&
  163. this.state.date.valueOf() !==
  164. (this.props.value && this.props.value.valueOf())
  165. ) {
  166. this.props.onChange(this.state.date.toJSDate());
  167. }
  168. }
  169. renderDateTimePicker(timezoneDate: DateTime | null) {
  170. if (!this.state.showPicker) {
  171. return null;
  172. }
  173. const { body } = document;
  174. return ReactDOM.createPortal(
  175. <Portal
  176. ref={(e: HTMLElement | null | undefined) => {
  177. this.portalRef = e;
  178. }}
  179. >
  180. <DatetimeStyled
  181. input={false}
  182. value={timezoneDate?.toJSDate()}
  183. style={{ top: 0, right: 0 }}
  184. onChange={(date: any) => {
  185. if (date) {
  186. this.handleChange(date.toDate());
  187. }
  188. }}
  189. dateFormat="DD/MM/YYYY"
  190. timeFormat="hh:mm A"
  191. locale="en-gb"
  192. isValidDate={(currentDate: any, selectedDate: any) =>
  193. this.isValidDate(currentDate.toDate(), selectedDate?.toDate())
  194. }
  195. />
  196. </Portal>,
  197. body
  198. );
  199. }
  200. render() {
  201. let timezoneDate = this.state.date;
  202. if (this.props.timezone === "utc" && timezoneDate) {
  203. timezoneDate = timezoneDate.setZone("utc");
  204. }
  205. return (
  206. <>
  207. <GlobalStyle />
  208. <Wrapper>
  209. <DropdownButtonStyled
  210. customRef={e => {
  211. this.buttonRef = e;
  212. }}
  213. width={207}
  214. value={
  215. timezoneDate ? timezoneDate.toFormat("dd/LL/yyyy hh:mm a") : "-"
  216. }
  217. centered
  218. useBold={this.props.useBold}
  219. onClick={() => {
  220. this.handleDropdownClick();
  221. }}
  222. onMouseDown={() => {
  223. this.itemMouseDown = true;
  224. }}
  225. onMouseUp={() => {
  226. this.itemMouseDown = false;
  227. }}
  228. />
  229. {this.renderDateTimePicker(timezoneDate)}
  230. </Wrapper>
  231. </>
  232. );
  233. }
  234. }
  235. export default DatetimePicker;