/*
Copyright (C) 2017 Cloudbase Solutions SRL
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
import autobind from "autobind-decorator";
import { DateTime } from "luxon";
import { observer } from "mobx-react";
import React from "react";
import Datetime from "react-datetime";
import ReactDOM from "react-dom";
import styled, { createGlobalStyle } from "styled-components";
import { ThemeProps } from "@src/components/Theme";
import DropdownButton from "@src/components/ui/Dropdowns/DropdownButton";
import DateUtils from "@src/utils/DateUtils";
import DomUtils from "@src/utils/DomUtils";
import style from "./style";
const GlobalStyle = createGlobalStyle`${style}`;
const Wrapper = styled.div`
width: ${ThemeProps.inputSizes.regular.width}px;
`;
const DropdownButtonStyled = styled(DropdownButton)`
font-size: 12px;
`;
const Portal = styled.div`
position: absolute;
z-index: 10;
&.hideTip {
.rdtPicker:after {
content: none;
}
}
`;
const DatetimeStyled = styled(Datetime)`
${ThemeProps.boxShadow}
`;
type Props = {
value: Date | null;
onChange: (date: Date) => void;
isValidDate?: (currentDate: Date, selectedDate?: Date) => boolean;
timezone: "utc" | "local";
useBold?: boolean;
dispatchChangeContinously?: boolean;
};
type State = {
showPicker: boolean;
date: DateTime | null;
};
@observer
class DatetimePicker extends React.Component {
state: State = {
showPicker: false,
date: null,
};
itemMouseDown: boolean | undefined;
portalRef: HTMLElement | undefined | null;
buttonRef: HTMLElement | undefined | null;
scrollableParent: HTMLElement | undefined | null;
UNSAFE_componentWillMount() {
if (this.props.value) {
this.setState({
date: DateUtils.getLocalDate(this.props.value),
});
}
}
componentDidMount() {
window.addEventListener("mousedown", this.handlePageClick, false);
if (this.buttonRef) {
this.scrollableParent = DomUtils.getScrollableParent(this.buttonRef);
this.scrollableParent.addEventListener("scroll", this.handleScroll);
}
}
UNSAFE_componentWillReceiveProps(newProps: Props) {
if (newProps.value?.getTime() !== this.props.value?.getTime()) {
this.setState({
date: newProps.value && DateUtils.getLocalDate(newProps.value),
});
}
}
componentDidUpdate() {
this.setPortalPosition();
}
componentWillUnmount() {
window.removeEventListener("mousedown", this.handlePageClick, false);
if (this.scrollableParent) {
this.scrollableParent.removeEventListener(
"scroll",
this.handleScroll,
false
);
}
}
setPortalPosition() {
if (!this.portalRef || !this.buttonRef) {
return;
}
const buttonRect = this.buttonRef.getBoundingClientRect();
const leftOffset =
buttonRect.left - (this.portalRef.offsetWidth - buttonRect.width) + 10;
const tipHeight = 12;
let topOffset = buttonRect.top + this.buttonRef.offsetHeight + tipHeight;
const listHeight = this.portalRef.offsetHeight;
if (topOffset + listHeight > window.innerHeight) {
topOffset = window.innerHeight - listHeight - 16;
this.portalRef.classList.add("hideTip");
} else {
this.portalRef.classList.remove("hideTip");
}
this.portalRef.style.top = `${topOffset + window.pageYOffset}px`;
this.portalRef.style.left = `${leftOffset + window.pageXOffset}px`;
}
isValidDate(currentDate: Date, selectedDate?: Date): boolean {
if (!this.props.isValidDate) {
return true;
}
return this.props.isValidDate(currentDate, selectedDate);
}
@autobind
handleScroll() {
if (this.buttonRef) {
if (DomUtils.isElementInViewport(this.buttonRef, this.scrollableParent)) {
this.setPortalPosition();
} else if (this.state.showPicker) {
this.setState({ showPicker: false });
}
}
}
@autobind
handlePageClick(e: Event) {
const path = DomUtils.getEventPath(e);
if (!this.itemMouseDown && !path.find(n => n.className === "rdtPicker")) {
this.dispatchChange();
this.setState({ showPicker: false });
}
}
handleDropdownClick() {
this.dispatchChange();
this.setState(prevState => ({ showPicker: !prevState.showPicker }));
}
handleChange(newDate: Date) {
let date = DateUtils.getLocalDate(newDate);
if (this.props.timezone === "utc") {
date = date.setZone("utc");
}
this.setState({ date }, () => {
if (this.props.dispatchChangeContinously) {
this.dispatchChange();
}
});
}
dispatchChange() {
if (
this.state.date &&
this.state.showPicker &&
this.state.date.valueOf() !==
(this.props.value && this.props.value.valueOf())
) {
this.props.onChange(this.state.date.toJSDate());
}
}
renderDateTimePicker(timezoneDate: DateTime | null) {
if (!this.state.showPicker) {
return null;
}
const { body } = document;
return ReactDOM.createPortal(
{
this.portalRef = e;
}}
>
{
if (date) {
this.handleChange(date.toDate());
}
}}
dateFormat="DD/MM/YYYY"
timeFormat="hh:mm A"
locale="en-gb"
isValidDate={(currentDate: any, selectedDate: any) =>
this.isValidDate(currentDate.toDate(), selectedDate?.toDate())
}
/>
,
body
);
}
render() {
let timezoneDate = this.state.date;
if (this.props.timezone === "utc" && timezoneDate) {
timezoneDate = timezoneDate.setZone("utc");
}
return (
<>
{
this.buttonRef = e;
}}
width={207}
value={
timezoneDate ? timezoneDate.toFormat("dd/LL/yyyy hh:mm a") : "-"
}
centered
useBold={this.props.useBold}
onClick={() => {
this.handleDropdownClick();
}}
onMouseDown={() => {
this.itemMouseDown = true;
}}
onMouseUp={() => {
this.itemMouseDown = false;
}}
/>
{this.renderDateTimePicker(timezoneDate)}
>
);
}
}
export default DatetimePicker;