feat(DatePicker): add DatePicker draggable cell
This commit is contained in:
parent
38457fbcff
commit
d012eb21a2
@ -12,4 +12,7 @@ module.exports = {
|
||||
'./configs/jest/__mocks__/fileMock.js',
|
||||
'\\.(css|less|scss)$': 'identity-obj-proxy',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'/node_modules/(?!react-dnd|dnd-core|@react-dnd|react-dnd-html5-backend)',
|
||||
],
|
||||
};
|
||||
|
||||
BIN
offline_node_modules/@react-dnd-asap-npm-5.0.2-66021d3d61-0063db616d.zip
generated
Normal file
BIN
offline_node_modules/@react-dnd-asap-npm-5.0.2-66021d3d61-0063db616d.zip
generated
Normal file
Binary file not shown.
BIN
offline_node_modules/@react-dnd-invariant-npm-4.0.2-826eacc1ea-b303cc53fc.zip
generated
Normal file
BIN
offline_node_modules/@react-dnd-invariant-npm-4.0.2-826eacc1ea-b303cc53fc.zip
generated
Normal file
Binary file not shown.
BIN
offline_node_modules/@react-dnd-shallowequal-npm-4.0.2-f944714335-9a352fd176.zip
generated
Normal file
BIN
offline_node_modules/@react-dnd-shallowequal-npm-4.0.2-f944714335-9a352fd176.zip
generated
Normal file
Binary file not shown.
BIN
offline_node_modules/dnd-core-npm-16.0.1-552224cee0-6b852c576c.zip
generated
Normal file
BIN
offline_node_modules/dnd-core-npm-16.0.1-552224cee0-6b852c576c.zip
generated
Normal file
Binary file not shown.
BIN
offline_node_modules/hoist-non-react-statics-npm-3.3.2-e7b709e6c1-fe0889169e.zip
generated
Normal file
BIN
offline_node_modules/hoist-non-react-statics-npm-3.3.2-e7b709e6c1-fe0889169e.zip
generated
Normal file
Binary file not shown.
BIN
offline_node_modules/react-dnd-html5-backend-npm-16.0.1-754940d855-6e4b632a11.zip
generated
Normal file
BIN
offline_node_modules/react-dnd-html5-backend-npm-16.0.1-754940d855-6e4b632a11.zip
generated
Normal file
Binary file not shown.
BIN
offline_node_modules/react-dnd-npm-16.0.1-974f047d7b-d069435750.zip
generated
Normal file
BIN
offline_node_modules/react-dnd-npm-16.0.1-974f047d7b-d069435750.zip
generated
Normal file
Binary file not shown.
BIN
offline_node_modules/redux-npm-4.2.1-e7e2cf2e37-136d98b3d5.zip
generated
Normal file
BIN
offline_node_modules/redux-npm-4.2.1-e7e2cf2e37-136d98b3d5.zip
generated
Normal file
Binary file not shown.
@ -35,6 +35,8 @@
|
||||
"normalize.css": "8.0.1",
|
||||
"prop-types": "15.8.1",
|
||||
"react": "18.2.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-router-dom": "6.22.3"
|
||||
},
|
||||
|
||||
3
src/shared/constants/drag.ts
Normal file
3
src/shared/constants/drag.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const ItemTypes = {
|
||||
CELL: 'date-cell',
|
||||
};
|
||||
@ -6,3 +6,4 @@ export type {
|
||||
WeekDaysKeys,
|
||||
WeekDaysValues,
|
||||
} from './date';
|
||||
export { ItemTypes } from './drag';
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
.firstDate {
|
||||
background-color: blue;
|
||||
color: white;
|
||||
border-top-left-radius: 10px
|
||||
}
|
||||
|
||||
.inSelection {
|
||||
background: gold;
|
||||
color: darkgrey;
|
||||
}
|
||||
|
||||
.lastDate {
|
||||
background-color: blue;
|
||||
color: white;
|
||||
border-bottom-right-radius: 10px
|
||||
}
|
||||
@ -1,262 +1,44 @@
|
||||
import classnames from 'classnames';
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { DataDateString } from 'shared/types/date';
|
||||
import { daysCountBetween, getDataDateString } from 'shared/utils/date';
|
||||
import { exhaustiveCheck } from 'shared/utils/type';
|
||||
import { useContext } from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
|
||||
import { CalendarWidget } from '../../CalendarWidget';
|
||||
|
||||
import classes from './DatePicker.style.module.css';
|
||||
import { CustomDragLayer } from './components/CustomDragLayer';
|
||||
import { DraggableCell } from './components/DraggableCell';
|
||||
import DatePickerContextProvider, { DatePickerContext } from './DatePickerContext';
|
||||
|
||||
import type { DatePickerContextProviderProps } from './DatePickerContext';
|
||||
import type { CalendarWidgetProps } from '../../CalendarWidget';
|
||||
import type { FC } from 'react';
|
||||
|
||||
export type DatePickerProps = CalendarWidgetProps & {
|
||||
firstDate?: Date;
|
||||
lastDate?: Date;
|
||||
onSelect?: (params: { dataDate: DataDateString; date: Date; type: 'firstDate' | 'lastDate' }) => void;
|
||||
onUnselect?: (params: { dataDate: DataDateString; date: Date; type: 'firstDate' | 'lastDate' }) => void;
|
||||
};
|
||||
export type DatePickerProps = CalendarWidgetProps & Omit<DatePickerContextProviderProps, 'children'>;
|
||||
|
||||
const DatePicker: FC<DatePickerProps> = (props) => {
|
||||
const {
|
||||
firstDate,
|
||||
lastDate,
|
||||
onSelect,
|
||||
onUnselect,
|
||||
...restWidgetProps
|
||||
} = props;
|
||||
|
||||
const [firstSelectedDate, setFirstSelectedDate] = useState<DataDateString | null>(
|
||||
firstDate
|
||||
? getDataDateString(firstDate)
|
||||
: null,
|
||||
);
|
||||
const [lastSelectedDate, setLastSelectedDate] = useState<DataDateString | null>(
|
||||
lastDate
|
||||
? getDataDateString(lastDate)
|
||||
: null,
|
||||
);
|
||||
|
||||
const isSelectedFirstDate = firstSelectedDate !== null;
|
||||
const isSelectedLastDate = lastSelectedDate !== null;
|
||||
const isSelectedBothDate = isSelectedFirstDate && isSelectedLastDate;
|
||||
|
||||
const isCellInSelection = (dataDate: DataDateString) => {
|
||||
if (!isSelectedFirstDate || !isSelectedLastDate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const dateTime = new Date(dataDate).getTime();
|
||||
const firstDateTime = new Date(firstSelectedDate).getTime();
|
||||
const lastDateTime = new Date(lastSelectedDate).getTime();
|
||||
|
||||
return dateTime >= firstDateTime && dateTime <= lastDateTime;
|
||||
};
|
||||
|
||||
const toggleCellDate = (params: { dataDate: DataDateString | null; type: 'firstDate' | 'lastDate' }) => {
|
||||
const { dataDate, type } = params;
|
||||
|
||||
if (dataDate === null) {
|
||||
let typedDataDate: DataDateString;
|
||||
|
||||
switch (type) {
|
||||
case 'firstDate':
|
||||
if (!isSelectedFirstDate) {
|
||||
return;
|
||||
}
|
||||
typedDataDate = firstSelectedDate;
|
||||
setFirstSelectedDate(null);
|
||||
break;
|
||||
|
||||
case 'lastDate':
|
||||
if (!isSelectedLastDate) {
|
||||
return;
|
||||
}
|
||||
typedDataDate = lastSelectedDate;
|
||||
setLastSelectedDate(null);
|
||||
|
||||
break;
|
||||
default:
|
||||
exhaustiveCheck(type);
|
||||
break;
|
||||
}
|
||||
|
||||
onUnselect?.({
|
||||
type,
|
||||
// @ts-ignore
|
||||
dataDate: typedDataDate,
|
||||
// @ts-ignore
|
||||
date: new Date(typedDataDate),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'firstDate':
|
||||
setFirstSelectedDate(dataDate);
|
||||
break;
|
||||
|
||||
case 'lastDate':
|
||||
setLastSelectedDate(dataDate);
|
||||
break;
|
||||
|
||||
default:
|
||||
exhaustiveCheck(type);
|
||||
break;
|
||||
}
|
||||
onSelect?.({
|
||||
type,
|
||||
dataDate,
|
||||
date: new Date(dataDate),
|
||||
});
|
||||
};
|
||||
|
||||
const toggleCellSelection = (dataDate: DataDateString) => {
|
||||
if (!isSelectedFirstDate && !isSelectedLastDate) {
|
||||
toggleCellDate({ dataDate, type: 'firstDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSelectedFirstDate && !isSelectedLastDate) {
|
||||
if (dataDate < firstSelectedDate) {
|
||||
toggleCellDate({ dataDate: firstSelectedDate, type: 'lastDate' });
|
||||
toggleCellDate({ dataDate, type: 'firstDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataDate > firstSelectedDate) {
|
||||
toggleCellDate({ dataDate, type: 'lastDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
toggleCellDate({ dataDate: null, type: 'firstDate' });
|
||||
}
|
||||
|
||||
if (!isSelectedFirstDate && isSelectedLastDate) {
|
||||
if (dataDate < lastSelectedDate) {
|
||||
toggleCellDate({ dataDate, type: 'firstDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataDate > lastSelectedDate) {
|
||||
toggleCellDate({ dataDate: lastSelectedDate, type: 'firstDate' });
|
||||
toggleCellDate({ dataDate, type: 'lastDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
toggleCellDate({ dataDate: null, type: 'lastDate' });
|
||||
}
|
||||
|
||||
if (isSelectedBothDate) {
|
||||
if (dataDate < firstSelectedDate) {
|
||||
toggleCellDate({ dataDate, type: 'firstDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataDate > lastSelectedDate) {
|
||||
toggleCellDate({ dataDate, type: 'lastDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataDate === firstSelectedDate) {
|
||||
toggleCellDate({ dataDate: null, type: 'firstDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataDate === lastSelectedDate) {
|
||||
toggleCellDate({ dataDate: null, type: 'lastDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const daysToFirstDate = daysCountBetween(new Date(firstSelectedDate), new Date(dataDate));
|
||||
const daysToLastDate = daysCountBetween(new Date(lastSelectedDate), new Date(dataDate));
|
||||
|
||||
if (daysToFirstDate < daysToLastDate) {
|
||||
toggleCellDate({ dataDate, type: 'firstDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (daysToFirstDate > daysToLastDate) {
|
||||
toggleCellDate({ dataDate, type: 'lastDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
toggleCellDate({ dataDate, type: 'firstDate' });
|
||||
}
|
||||
};
|
||||
|
||||
const isFirstDate = (dataDate: DataDateString) => dataDate === firstSelectedDate;
|
||||
|
||||
const isLastDate = (dataDate: DataDateString) => dataDate === lastSelectedDate;
|
||||
|
||||
const getDataTestId = (dataDate: DataDateString) => {
|
||||
if (isFirstDate(dataDate)) {
|
||||
return {
|
||||
'data-testid': 'first-selected-cell',
|
||||
};
|
||||
}
|
||||
|
||||
if (isLastDate(dataDate)) {
|
||||
return {
|
||||
'data-testid': 'last-selected-cell',
|
||||
};
|
||||
}
|
||||
|
||||
if (isCellInSelection(dataDate)) {
|
||||
return {
|
||||
'data-testid': 'cell-in-range',
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
const MyCalendar: FC<CalendarWidgetProps> = (props) => {
|
||||
const { toggleCellSelection, getDataTestId } = useContext(DatePickerContext);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setLastSelectedDate(null);
|
||||
setFirstSelectedDate(null);
|
||||
}}
|
||||
onKeyPress={() => {}}
|
||||
>
|
||||
reset
|
||||
</button>
|
||||
|
||||
<CalendarWidget
|
||||
DateCellProps={({ dataDate }) => ({
|
||||
onClick: () => {
|
||||
toggleCellSelection(dataDate);
|
||||
},
|
||||
className: classnames({
|
||||
[classes.firstDate as string]: isFirstDate(dataDate),
|
||||
[classes.inSelection as string]: isCellInSelection(dataDate)
|
||||
&& !isFirstDate(dataDate)
|
||||
&& !isLastDate(dataDate),
|
||||
[classes.lastDate as string]: isLastDate(dataDate),
|
||||
}),
|
||||
...getDataTestId(dataDate),
|
||||
})}
|
||||
{...restWidgetProps}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CalendarWidget
|
||||
DateCellComponent={DraggableCell}
|
||||
DateCellProps={({ dataDate }) => ({
|
||||
onClick: () => {
|
||||
toggleCellSelection(dataDate);
|
||||
},
|
||||
...getDataTestId(dataDate),
|
||||
})}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const DatePicker: FC<DatePickerProps> = (props) => (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<DatePickerContextProvider {...props}>
|
||||
<MyCalendar {...props} />
|
||||
|
||||
<CustomDragLayer />
|
||||
</DatePickerContextProvider>
|
||||
</DndProvider>
|
||||
);
|
||||
|
||||
export default DatePicker;
|
||||
|
||||
282
src/widgets/DatePicker/ui/DatePickerContext.tsx
Normal file
282
src/widgets/DatePicker/ui/DatePickerContext.tsx
Normal file
@ -0,0 +1,282 @@
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import type { DataDateString } from 'shared/types/date';
|
||||
import { daysCountBetween, getDataDateString } from 'shared/utils/date';
|
||||
import { exhaustiveCheck } from 'shared/utils/type';
|
||||
|
||||
import type { FC, ReactNode } from 'react';
|
||||
|
||||
type DatePickerContextValue = {
|
||||
firstSelectedDate: DataDateString | null;
|
||||
lastSelectedDate: DataDateString | null;
|
||||
isSelectedFirstDate: boolean;
|
||||
isSelectedLastDate: boolean;
|
||||
isSelectedBothDate: boolean;
|
||||
isCellInSelection: (dataDate: DataDateString) => boolean;
|
||||
toggleCellDate: (params: { dataDate: DataDateString | null; type: 'firstDate' | 'lastDate' }) => void;
|
||||
toggleCellSelection: (dataDate: DataDateString) => void;
|
||||
isFirstDate: (dataDate: DataDateString) => boolean;
|
||||
isLastDate: (dataDate: DataDateString) => boolean;
|
||||
getDataTestId: (dataDate: DataDateString) => Record<'data-testid', string> | null;
|
||||
};
|
||||
export const DatePickerContext = createContext<DatePickerContextValue>({} as DatePickerContextValue);
|
||||
|
||||
export type DatePickerContextProviderProps = {
|
||||
firstDate?: Date;
|
||||
lastDate?: Date;
|
||||
onSelect?: (params: { dataDate: DataDateString; date: Date; type: 'firstDate' | 'lastDate' }) => void;
|
||||
onUnselect?: (params: { dataDate: DataDateString; date: Date; type: 'firstDate' | 'lastDate' }) => void;
|
||||
children: ReactNode;
|
||||
};
|
||||
const DatePickerContextProvider: FC<DatePickerContextProviderProps> = (props) => {
|
||||
const {
|
||||
children,
|
||||
firstDate,
|
||||
lastDate,
|
||||
onUnselect,
|
||||
onSelect,
|
||||
} = props;
|
||||
|
||||
const [firstSelectedDate, setFirstSelectedDate] = useState<DataDateString | null>(
|
||||
firstDate
|
||||
? getDataDateString(firstDate)
|
||||
: null,
|
||||
);
|
||||
const [lastSelectedDate, setLastSelectedDate] = useState<DataDateString | null>(
|
||||
lastDate
|
||||
? getDataDateString(lastDate)
|
||||
: null,
|
||||
);
|
||||
|
||||
const isSelectedFirstDate = firstSelectedDate !== null;
|
||||
const isSelectedLastDate = lastSelectedDate !== null;
|
||||
const isSelectedBothDate = isSelectedFirstDate && isSelectedLastDate;
|
||||
|
||||
const isCellInSelection = useCallback((dataDate: DataDateString) => {
|
||||
if (!isSelectedFirstDate || !isSelectedLastDate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const dateTime = new Date(dataDate).getTime();
|
||||
const firstDateTime = new Date(firstSelectedDate).getTime();
|
||||
const lastDateTime = new Date(lastSelectedDate).getTime();
|
||||
|
||||
return dateTime >= firstDateTime && dateTime <= lastDateTime;
|
||||
}, [firstSelectedDate, isSelectedFirstDate, isSelectedLastDate, lastSelectedDate]);
|
||||
|
||||
const toggleCellDate = useCallback((params: { dataDate: DataDateString | null; type: 'firstDate' | 'lastDate' }) => {
|
||||
const { dataDate, type } = params;
|
||||
|
||||
if (dataDate === null) {
|
||||
let typedDataDate: DataDateString;
|
||||
|
||||
switch (type) {
|
||||
case 'firstDate':
|
||||
if (!isSelectedFirstDate) {
|
||||
return;
|
||||
}
|
||||
typedDataDate = firstSelectedDate;
|
||||
setFirstSelectedDate(null);
|
||||
break;
|
||||
|
||||
case 'lastDate':
|
||||
if (!isSelectedLastDate) {
|
||||
return;
|
||||
}
|
||||
typedDataDate = lastSelectedDate;
|
||||
setLastSelectedDate(null);
|
||||
|
||||
break;
|
||||
default:
|
||||
exhaustiveCheck(type);
|
||||
break;
|
||||
}
|
||||
|
||||
onUnselect?.({
|
||||
type,
|
||||
// @ts-ignore
|
||||
dataDate: typedDataDate,
|
||||
// @ts-ignore
|
||||
date: new Date(typedDataDate),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'firstDate':
|
||||
setFirstSelectedDate(dataDate);
|
||||
break;
|
||||
|
||||
case 'lastDate':
|
||||
setLastSelectedDate(dataDate);
|
||||
break;
|
||||
|
||||
default:
|
||||
exhaustiveCheck(type);
|
||||
break;
|
||||
}
|
||||
onSelect?.({
|
||||
type,
|
||||
dataDate,
|
||||
date: new Date(dataDate),
|
||||
});
|
||||
}, [firstSelectedDate, isSelectedFirstDate, isSelectedLastDate, lastSelectedDate, onSelect, onUnselect]);
|
||||
|
||||
const toggleCellSelection = useCallback((dataDate: DataDateString) => {
|
||||
if (!isSelectedFirstDate && !isSelectedLastDate) {
|
||||
toggleCellDate({ dataDate, type: 'firstDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSelectedFirstDate && !isSelectedLastDate) {
|
||||
if (dataDate < firstSelectedDate) {
|
||||
toggleCellDate({ dataDate: firstSelectedDate, type: 'lastDate' });
|
||||
toggleCellDate({ dataDate, type: 'firstDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataDate > firstSelectedDate) {
|
||||
toggleCellDate({ dataDate, type: 'lastDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
toggleCellDate({ dataDate: null, type: 'firstDate' });
|
||||
}
|
||||
|
||||
if (!isSelectedFirstDate && isSelectedLastDate) {
|
||||
if (dataDate < lastSelectedDate) {
|
||||
toggleCellDate({ dataDate, type: 'firstDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataDate > lastSelectedDate) {
|
||||
toggleCellDate({ dataDate: lastSelectedDate, type: 'firstDate' });
|
||||
toggleCellDate({ dataDate, type: 'lastDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
toggleCellDate({ dataDate: null, type: 'lastDate' });
|
||||
}
|
||||
|
||||
if (isSelectedBothDate) {
|
||||
if (dataDate < firstSelectedDate) {
|
||||
toggleCellDate({ dataDate, type: 'firstDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataDate > lastSelectedDate) {
|
||||
toggleCellDate({ dataDate, type: 'lastDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataDate === firstSelectedDate) {
|
||||
toggleCellDate({ dataDate: null, type: 'firstDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataDate === lastSelectedDate) {
|
||||
toggleCellDate({ dataDate: null, type: 'lastDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const daysToFirstDate = daysCountBetween(new Date(firstSelectedDate), new Date(dataDate));
|
||||
const daysToLastDate = daysCountBetween(new Date(lastSelectedDate), new Date(dataDate));
|
||||
|
||||
if (daysToFirstDate < daysToLastDate) {
|
||||
toggleCellDate({ dataDate, type: 'firstDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (daysToFirstDate > daysToLastDate) {
|
||||
toggleCellDate({ dataDate, type: 'lastDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
toggleCellDate({ dataDate, type: 'firstDate' });
|
||||
}
|
||||
}, [
|
||||
firstSelectedDate,
|
||||
isSelectedBothDate,
|
||||
isSelectedFirstDate,
|
||||
isSelectedLastDate,
|
||||
lastSelectedDate,
|
||||
toggleCellDate,
|
||||
]);
|
||||
|
||||
const isFirstDate = useCallback((dataDate: DataDateString) => dataDate === firstSelectedDate, [firstSelectedDate]);
|
||||
|
||||
const isLastDate = useCallback((dataDate: DataDateString) => dataDate === lastSelectedDate, [lastSelectedDate]);
|
||||
|
||||
const getDataTestId = useCallback((dataDate: DataDateString) => {
|
||||
if (isFirstDate(dataDate)) {
|
||||
return {
|
||||
'data-testid': 'first-selected-cell',
|
||||
};
|
||||
}
|
||||
|
||||
if (isLastDate(dataDate)) {
|
||||
return {
|
||||
'data-testid': 'last-selected-cell',
|
||||
};
|
||||
}
|
||||
|
||||
if (isCellInSelection(dataDate)) {
|
||||
return {
|
||||
'data-testid': 'cell-in-range',
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [isCellInSelection, isFirstDate, isLastDate]);
|
||||
|
||||
const value: DatePickerContextValue = useMemo(() => ({
|
||||
firstSelectedDate,
|
||||
lastSelectedDate,
|
||||
isSelectedFirstDate,
|
||||
isSelectedLastDate,
|
||||
isSelectedBothDate,
|
||||
isCellInSelection,
|
||||
toggleCellDate,
|
||||
toggleCellSelection,
|
||||
isFirstDate,
|
||||
isLastDate,
|
||||
getDataTestId,
|
||||
}), [
|
||||
firstSelectedDate,
|
||||
getDataTestId,
|
||||
isCellInSelection,
|
||||
isFirstDate,
|
||||
isLastDate,
|
||||
isSelectedBothDate,
|
||||
isSelectedFirstDate,
|
||||
isSelectedLastDate,
|
||||
lastSelectedDate,
|
||||
toggleCellDate,
|
||||
toggleCellSelection,
|
||||
]);
|
||||
|
||||
return (
|
||||
<DatePickerContext.Provider value={value}>
|
||||
{children}
|
||||
</DatePickerContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatePickerContextProvider;
|
||||
@ -0,0 +1 @@
|
||||
export { default as CustomDragLayer } from './ui/CustomDragLayer.ui';
|
||||
@ -0,0 +1,13 @@
|
||||
.root {
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
background-color: #1892ff;
|
||||
opacity: 0.8;
|
||||
border-radius: 50%;
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
pointer-events: none;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: grabbing;
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
import { useDragLayer } from 'react-dnd';
|
||||
|
||||
import { ItemTypes } from 'shared/constants';
|
||||
|
||||
import classes from './CustomDragLayer.style.module.css';
|
||||
|
||||
import type { XYCoord } from 'react-dnd';
|
||||
|
||||
const snapToGrid = (x: number, y: number) => {
|
||||
const snappedX = Math.round(x / 53) * 53;
|
||||
const snappedY = Math.round(y / 41) * 41;
|
||||
|
||||
return [snappedX, snappedY];
|
||||
};
|
||||
|
||||
const getDragLayerStyles = (
|
||||
initialOffset: XYCoord | null,
|
||||
currentOffset: XYCoord | null,
|
||||
) => {
|
||||
if (!initialOffset || !currentOffset) {
|
||||
return {
|
||||
display: 'none',
|
||||
};
|
||||
}
|
||||
|
||||
let { x = 0, y = 0 } = currentOffset;
|
||||
|
||||
x -= initialOffset.x || 0;
|
||||
y -= initialOffset.y || 0;
|
||||
// @ts-ignore
|
||||
[x, y] = snapToGrid(x, y);
|
||||
x += initialOffset.x || 0;
|
||||
y += initialOffset.y || 0;
|
||||
|
||||
const transform = `translate(${Math.round(x) + 8.5}px, ${Math.round(y) + 3}px)`;
|
||||
|
||||
return {
|
||||
transform,
|
||||
WebkitTransform: transform,
|
||||
};
|
||||
};
|
||||
|
||||
const CustomDragLayer = () => {
|
||||
const {
|
||||
itemType,
|
||||
isDragging,
|
||||
sourceClientOffset,
|
||||
initialSourceClientOffset,
|
||||
} = useDragLayer((monitor) => ({
|
||||
itemType: monitor.getItemType(),
|
||||
initialSourceClientOffset: monitor.getInitialSourceClientOffset(),
|
||||
sourceClientOffset: monitor.getSourceClientOffset(),
|
||||
isDragging: monitor.isDragging(),
|
||||
}));
|
||||
|
||||
if (itemType !== ItemTypes.CELL) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isDragging) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const transformStyles = getDragLayerStyles(initialSourceClientOffset, sourceClientOffset);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes.root}
|
||||
style={transformStyles}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomDragLayer;
|
||||
@ -0,0 +1 @@
|
||||
export { default as DraggableCell } from './ui/DraggableCell.ui';
|
||||
@ -0,0 +1,71 @@
|
||||
.dateCell {
|
||||
width: calc((100% / 7));
|
||||
height: 40px;
|
||||
background: aquamarine;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&:nth-child(odd):not(:last-child) {
|
||||
width: calc((100% / 7) - 1px);
|
||||
border-right: 1px solid black;
|
||||
}
|
||||
|
||||
&:nth-child(even) {
|
||||
width: calc((100% / 7) - 1px);
|
||||
border-right: 1px solid black;
|
||||
}
|
||||
|
||||
.today {
|
||||
color: red;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.outMothRange {
|
||||
color: darkgrey;
|
||||
}
|
||||
|
||||
.valueDate {
|
||||
color: gold;
|
||||
background: red;
|
||||
border-radius: 50%;
|
||||
padding: 5px;
|
||||
height: 1.2em;
|
||||
width: 1.2em;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.firstDate {
|
||||
background: #cfe8ff;
|
||||
}
|
||||
|
||||
.inSelection {
|
||||
background: #cfe8ff;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.lastDate {
|
||||
background: #cfe8ff;
|
||||
}
|
||||
|
||||
.selectedDateCell {
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
background-color: #1892ff;
|
||||
|
||||
span {
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dragging {
|
||||
background: red;
|
||||
}
|
||||
@ -0,0 +1,195 @@
|
||||
import classnames from 'classnames';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import {
|
||||
useDrag,
|
||||
useDragDropManager,
|
||||
useDrop,
|
||||
} from 'react-dnd';
|
||||
import { getEmptyImage } from 'react-dnd-html5-backend';
|
||||
|
||||
import { CalendarWidgetContext } from 'widgets/CalendarWidget/ui/CalendarWidgetContext';
|
||||
import type { DateCellProps } from 'widgets/CalendarWidget/ui/components/DateCell/ui/DateCell.ui';
|
||||
import { DatePickerContext } from 'widgets/DatePicker/ui/DatePickerContext';
|
||||
|
||||
import { ItemTypes } from 'shared/constants';
|
||||
import type { DataDateString } from 'shared/types/date';
|
||||
import { getDataDateString } from 'shared/utils/date';
|
||||
|
||||
import classes from './DraggableCell.style.module.css';
|
||||
|
||||
import type { FC } from 'react';
|
||||
|
||||
type DraggableCellProps = DateCellProps;
|
||||
const DraggableCell: FC<DraggableCellProps> = (props) => {
|
||||
const {
|
||||
date,
|
||||
className,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const {
|
||||
targetMonthIndex,
|
||||
value,
|
||||
highlightToday,
|
||||
highlightValue,
|
||||
} = useContext(CalendarWidgetContext);
|
||||
|
||||
const {
|
||||
toggleCellDate,
|
||||
isFirstDate,
|
||||
isLastDate,
|
||||
firstSelectedDate,
|
||||
lastSelectedDate,
|
||||
isCellInSelection,
|
||||
} = useContext(DatePickerContext);
|
||||
|
||||
const todayDate = new Date();
|
||||
const dataDate = getDataDateString(date);
|
||||
const dataToday = getDataDateString(todayDate);
|
||||
const dataValue = getDataDateString(value);
|
||||
const isOutMonthRange = date.getMonth() !== targetMonthIndex;
|
||||
const dragDropManager = useDragDropManager();
|
||||
|
||||
const [{ isDragging, canDrag }, drag, preview] = useDrag(() => ({
|
||||
type: ItemTypes.CELL,
|
||||
item: {
|
||||
dataDate,
|
||||
isFirstDate: isFirstDate(dataDate),
|
||||
isLastDate: isLastDate(dataDate),
|
||||
firstSelectedDate,
|
||||
lastSelectedDate,
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
canDrag: isFirstDate(dataDate) || isLastDate(dataDate),
|
||||
}),
|
||||
}), [isFirstDate, isLastDate, dataDate]);
|
||||
|
||||
const onDropHandler = (item: { dataDate: DataDateString }) => {
|
||||
const type = isFirstDate(item.dataDate) ? 'firstDate' : 'lastDate';
|
||||
if (firstSelectedDate !== null && lastSelectedDate !== null) {
|
||||
if (dataDate < firstSelectedDate && type === 'lastDate') {
|
||||
toggleCellDate({ dataDate, type: 'firstDate' });
|
||||
toggleCellDate({ dataDate: firstSelectedDate, type: 'lastDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataDate > lastSelectedDate && type === 'firstDate') {
|
||||
toggleCellDate({ dataDate, type: 'lastDate' });
|
||||
toggleCellDate({ dataDate: lastSelectedDate, type: 'firstDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
toggleCellDate({ dataDate, type });
|
||||
};
|
||||
|
||||
const [{ isOver }, drop] = useDrop(() => ({
|
||||
accept: ItemTypes.CELL,
|
||||
drop: onDropHandler,
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver(),
|
||||
}),
|
||||
}), [onDropHandler]);
|
||||
|
||||
useEffect(() => {
|
||||
preview(getEmptyImage(), { captureDraggingState: true });
|
||||
}, [preview]);
|
||||
|
||||
useEffect(() => {
|
||||
type DragItem = {
|
||||
dataDate: DataDateString;
|
||||
isFirstDate: boolean;
|
||||
isLastDate: boolean;
|
||||
firstSelectedDate: DataDateString;
|
||||
lastSelectedDate: DataDateString;
|
||||
};
|
||||
const dragItem = dragDropManager.getMonitor().getItem() as DragItem | null;
|
||||
|
||||
if (!isOver || !dragItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isDragLast = dragItem.isLastDate;
|
||||
const isDragFirst = dragItem.isFirstDate;
|
||||
|
||||
if (isDragFirst) {
|
||||
if (dataDate < dragItem.lastSelectedDate) {
|
||||
toggleCellDate({ dataDate, type: 'firstDate' });
|
||||
toggleCellDate({ dataDate: dragItem.lastSelectedDate, type: 'lastDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataDate > dragItem.lastSelectedDate) {
|
||||
toggleCellDate({ dataDate, type: 'lastDate' });
|
||||
toggleCellDate({ dataDate: dragItem.lastSelectedDate, type: 'firstDate' });
|
||||
}
|
||||
}
|
||||
|
||||
if (isDragLast) {
|
||||
if (dataDate < dragItem.firstSelectedDate) {
|
||||
toggleCellDate({ dataDate, type: 'firstDate' });
|
||||
toggleCellDate({ dataDate: dragItem.firstSelectedDate, type: 'lastDate' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataDate > dragItem.firstSelectedDate) {
|
||||
toggleCellDate({ dataDate, type: 'lastDate' });
|
||||
toggleCellDate({ dataDate: dragItem.firstSelectedDate, type: 'firstDate' });
|
||||
}
|
||||
}
|
||||
}, [dataDate, dragDropManager, isFirstDate, isLastDate, isOver, toggleCellDate]);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={dataDate}
|
||||
ref={canDrag ? drag : drop}
|
||||
className={classnames(
|
||||
classes.dateCell,
|
||||
className,
|
||||
{
|
||||
[classes.firstDate as string]: isFirstDate(dataDate),
|
||||
[classes.inSelection as string]: isCellInSelection(dataDate)
|
||||
&& !isFirstDate(dataDate)
|
||||
&& !isLastDate(dataDate),
|
||||
[classes.lastDate as string]: isLastDate(dataDate),
|
||||
},
|
||||
)}
|
||||
data-date={dataDate}
|
||||
data-testid="date-cell"
|
||||
role="button"
|
||||
style={{
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
cursor: canDrag ? isDragging ? 'grabbing' : 'grab' : 'pointer',
|
||||
position: 'relative',
|
||||
}}
|
||||
tabIndex={0}
|
||||
{...restProps}
|
||||
>
|
||||
<div
|
||||
className={classnames({
|
||||
// @ts-ignore
|
||||
[classes.selectedDateCell]: isFirstDate(dataDate) || isLastDate(dataDate),
|
||||
})}
|
||||
>
|
||||
<span
|
||||
className={classnames(
|
||||
{
|
||||
[classes.today as string]: Boolean(highlightToday) && dataDate === dataToday,
|
||||
[classes.outMothRange as string]: isOutMonthRange,
|
||||
[classes.valueDate as string]: Boolean(highlightValue) && dataDate === dataValue,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{date.getDate()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DraggableCell;
|
||||
88
yarn.lock
88
yarn.lock
@ -2951,6 +2951,27 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-dnd/asap@npm:^5.0.1":
|
||||
version: 5.0.2
|
||||
resolution: "@react-dnd/asap@npm:5.0.2"
|
||||
checksum: 10c0/0063db616db9801c9be18f11a912c3e214f87e714b1e4bf9462952af7ead65cba0b43e1f7c34bc8748811b6926e74d929e5e126f85ebb91b870faf809ceb5177
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-dnd/invariant@npm:^4.0.1":
|
||||
version: 4.0.2
|
||||
resolution: "@react-dnd/invariant@npm:4.0.2"
|
||||
checksum: 10c0/b303cc53fc5074cefb2a76b45b9c73ebb5d35630b18f5dc282ed9a9ac9b0287b9da1f6ac63acfdea2341b8f8187f615afc12d5eb14ec6015964f5c1b167332e2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-dnd/shallowequal@npm:^4.0.1":
|
||||
version: 4.0.2
|
||||
resolution: "@react-dnd/shallowequal@npm:4.0.2"
|
||||
checksum: 10c0/9a352fd176752e5d9c2797d598aca034b7829111ae0c992d80f40d5f068fcd6a039b1841c741dfa1ab67a36a00664310aec4f0ce216e4112f80875c9fe6fd8dc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@remix-run/router@npm:1.15.3":
|
||||
version: 1.15.3
|
||||
resolution: "@remix-run/router@npm:1.15.3"
|
||||
@ -6548,6 +6569,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dnd-core@npm:^16.0.1":
|
||||
version: 16.0.1
|
||||
resolution: "dnd-core@npm:16.0.1"
|
||||
dependencies:
|
||||
"@react-dnd/asap": "npm:^5.0.1"
|
||||
"@react-dnd/invariant": "npm:^4.0.1"
|
||||
redux: "npm:^4.2.0"
|
||||
checksum: 10c0/6b852c576c88b0a42e618efb37e046334f5e9914b8d38ad139933dd9595b6caf2a484953a6301094d23119c17479549553d71e92fd77fa37318122ea1e579f65
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dns-packet@npm:^5.2.2":
|
||||
version: 5.6.1
|
||||
resolution: "dns-packet@npm:5.6.1"
|
||||
@ -8495,6 +8527,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hoist-non-react-statics@npm:^3.3.2":
|
||||
version: 3.3.2
|
||||
resolution: "hoist-non-react-statics@npm:3.3.2"
|
||||
dependencies:
|
||||
react-is: "npm:^16.7.0"
|
||||
checksum: 10c0/fe0889169e845d738b59b64badf5e55fa3cf20454f9203d1eb088df322d49d4318df774828e789898dcb280e8a5521bb59b3203385662ca5e9218a6ca5820e74
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hosted-git-info@npm:^2.1.4":
|
||||
version: 2.8.9
|
||||
resolution: "hosted-git-info@npm:2.8.9"
|
||||
@ -10921,6 +10962,8 @@ __metadata:
|
||||
normalize.css: "npm:8.0.1"
|
||||
prop-types: "npm:15.8.1"
|
||||
react: "npm:18.2.0"
|
||||
react-dnd: "npm:^16.0.1"
|
||||
react-dnd-html5-backend: "npm:^16.0.1"
|
||||
react-dom: "npm:18.2.0"
|
||||
react-router-dom: "npm:6.22.3"
|
||||
standard-version: "npm:9.5.0"
|
||||
@ -12657,6 +12700,40 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-dnd-html5-backend@npm:^16.0.1":
|
||||
version: 16.0.1
|
||||
resolution: "react-dnd-html5-backend@npm:16.0.1"
|
||||
dependencies:
|
||||
dnd-core: "npm:^16.0.1"
|
||||
checksum: 10c0/6e4b632a11e20211d71f5f3bedadf13ecec2fa73372fde388619838294b1375f15b717d1ce128e12c872ff7b15c32d26761d2026b33c14fc55e4fd5477c15289
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-dnd@npm:^16.0.1":
|
||||
version: 16.0.1
|
||||
resolution: "react-dnd@npm:16.0.1"
|
||||
dependencies:
|
||||
"@react-dnd/invariant": "npm:^4.0.1"
|
||||
"@react-dnd/shallowequal": "npm:^4.0.1"
|
||||
dnd-core: "npm:^16.0.1"
|
||||
fast-deep-equal: "npm:^3.1.3"
|
||||
hoist-non-react-statics: "npm:^3.3.2"
|
||||
peerDependencies:
|
||||
"@types/hoist-non-react-statics": ">= 3.3.1"
|
||||
"@types/node": ">= 12"
|
||||
"@types/react": ">= 16"
|
||||
react: ">= 16.14"
|
||||
peerDependenciesMeta:
|
||||
"@types/hoist-non-react-statics":
|
||||
optional: true
|
||||
"@types/node":
|
||||
optional: true
|
||||
"@types/react":
|
||||
optional: true
|
||||
checksum: 10c0/d069435750f0d6653cfa2b951cac8abb3583fb144ff134a20176608877d9c5964c63384ebbacaa0fdeef819b592a103de0d8e06f3b742311d64a029ffed0baa3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-dom@npm:18.2.0":
|
||||
version: 18.2.0
|
||||
resolution: "react-dom@npm:18.2.0"
|
||||
@ -12669,7 +12746,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-is@npm:^16.13.1":
|
||||
"react-is@npm:^16.13.1, react-is@npm:^16.7.0":
|
||||
version: 16.13.1
|
||||
resolution: "react-is@npm:16.13.1"
|
||||
checksum: 10c0/33977da7a5f1a287936a0c85639fec6ca74f4f15ef1e59a6bc20338fc73dc69555381e211f7a3529b8150a1f71e4225525b41b60b52965bda53ce7d47377ada1
|
||||
@ -12828,6 +12905,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"redux@npm:^4.2.0":
|
||||
version: 4.2.1
|
||||
resolution: "redux@npm:4.2.1"
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.9.2"
|
||||
checksum: 10c0/136d98b3d5dbed1cd6279c8c18a6a74c416db98b8a432a46836bdd668475de6279a2d4fd9d1363f63904e00f0678a8a3e7fa532c897163340baf1e71bb42c742
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"reflect.getprototypeof@npm:^1.0.4":
|
||||
version: 1.0.6
|
||||
resolution: "reflect.getprototypeof@npm:1.0.6"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user