diff --git a/jest.config.js b/jest.config.js index 6123f6f..e395280 100644 --- a/jest.config.js +++ b/jest.config.js @@ -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)', + ], }; diff --git a/offline_node_modules/@react-dnd-asap-npm-5.0.2-66021d3d61-0063db616d.zip b/offline_node_modules/@react-dnd-asap-npm-5.0.2-66021d3d61-0063db616d.zip new file mode 100644 index 0000000..7ebf6d3 Binary files /dev/null and b/offline_node_modules/@react-dnd-asap-npm-5.0.2-66021d3d61-0063db616d.zip differ diff --git a/offline_node_modules/@react-dnd-invariant-npm-4.0.2-826eacc1ea-b303cc53fc.zip b/offline_node_modules/@react-dnd-invariant-npm-4.0.2-826eacc1ea-b303cc53fc.zip new file mode 100644 index 0000000..aac25d6 Binary files /dev/null and b/offline_node_modules/@react-dnd-invariant-npm-4.0.2-826eacc1ea-b303cc53fc.zip differ diff --git a/offline_node_modules/@react-dnd-shallowequal-npm-4.0.2-f944714335-9a352fd176.zip b/offline_node_modules/@react-dnd-shallowequal-npm-4.0.2-f944714335-9a352fd176.zip new file mode 100644 index 0000000..f29de96 Binary files /dev/null and b/offline_node_modules/@react-dnd-shallowequal-npm-4.0.2-f944714335-9a352fd176.zip differ diff --git a/offline_node_modules/dnd-core-npm-16.0.1-552224cee0-6b852c576c.zip b/offline_node_modules/dnd-core-npm-16.0.1-552224cee0-6b852c576c.zip new file mode 100644 index 0000000..fb3effa Binary files /dev/null and b/offline_node_modules/dnd-core-npm-16.0.1-552224cee0-6b852c576c.zip differ diff --git a/offline_node_modules/hoist-non-react-statics-npm-3.3.2-e7b709e6c1-fe0889169e.zip b/offline_node_modules/hoist-non-react-statics-npm-3.3.2-e7b709e6c1-fe0889169e.zip new file mode 100644 index 0000000..5f7b8a8 Binary files /dev/null and b/offline_node_modules/hoist-non-react-statics-npm-3.3.2-e7b709e6c1-fe0889169e.zip differ diff --git a/offline_node_modules/react-dnd-html5-backend-npm-16.0.1-754940d855-6e4b632a11.zip b/offline_node_modules/react-dnd-html5-backend-npm-16.0.1-754940d855-6e4b632a11.zip new file mode 100644 index 0000000..70d7593 Binary files /dev/null and b/offline_node_modules/react-dnd-html5-backend-npm-16.0.1-754940d855-6e4b632a11.zip differ diff --git a/offline_node_modules/react-dnd-npm-16.0.1-974f047d7b-d069435750.zip b/offline_node_modules/react-dnd-npm-16.0.1-974f047d7b-d069435750.zip new file mode 100644 index 0000000..856f220 Binary files /dev/null and b/offline_node_modules/react-dnd-npm-16.0.1-974f047d7b-d069435750.zip differ diff --git a/offline_node_modules/redux-npm-4.2.1-e7e2cf2e37-136d98b3d5.zip b/offline_node_modules/redux-npm-4.2.1-e7e2cf2e37-136d98b3d5.zip new file mode 100644 index 0000000..6e591d0 Binary files /dev/null and b/offline_node_modules/redux-npm-4.2.1-e7e2cf2e37-136d98b3d5.zip differ diff --git a/package.json b/package.json index b861bf9..3b080f6 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/src/shared/constants/drag.ts b/src/shared/constants/drag.ts new file mode 100644 index 0000000..7676626 --- /dev/null +++ b/src/shared/constants/drag.ts @@ -0,0 +1,3 @@ +export const ItemTypes = { + CELL: 'date-cell', +}; diff --git a/src/shared/constants/index.ts b/src/shared/constants/index.ts index 55361e3..44822ee 100644 --- a/src/shared/constants/index.ts +++ b/src/shared/constants/index.ts @@ -6,3 +6,4 @@ export type { WeekDaysKeys, WeekDaysValues, } from './date'; +export { ItemTypes } from './drag'; diff --git a/src/widgets/DatePicker/ui/DatePicker.style.module.css b/src/widgets/DatePicker/ui/DatePicker.style.module.css deleted file mode 100644 index 03fcd49..0000000 --- a/src/widgets/DatePicker/ui/DatePicker.style.module.css +++ /dev/null @@ -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 -} diff --git a/src/widgets/DatePicker/ui/DatePicker.ui.tsx b/src/widgets/DatePicker/ui/DatePicker.ui.tsx index 2436918..6def2a7 100644 --- a/src/widgets/DatePicker/ui/DatePicker.ui.tsx +++ b/src/widgets/DatePicker/ui/DatePicker.ui.tsx @@ -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; -const DatePicker: FC = (props) => { - const { - firstDate, - lastDate, - onSelect, - onUnselect, - ...restWidgetProps - } = props; - - const [firstSelectedDate, setFirstSelectedDate] = useState( - firstDate - ? getDataDateString(firstDate) - : null, - ); - const [lastSelectedDate, setLastSelectedDate] = useState( - 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 = (props) => { + const { toggleCellSelection, getDataTestId } = useContext(DatePickerContext); return ( -
- - - ({ - 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} - /> -
- + ({ + onClick: () => { + toggleCellSelection(dataDate); + }, + ...getDataTestId(dataDate), + })} + {...props} + /> ); }; +const DatePicker: FC = (props) => ( + + + + + + + +); + export default DatePicker; diff --git a/src/widgets/DatePicker/ui/DatePickerContext.tsx b/src/widgets/DatePicker/ui/DatePickerContext.tsx new file mode 100644 index 0000000..6c4d314 --- /dev/null +++ b/src/widgets/DatePicker/ui/DatePickerContext.tsx @@ -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({} 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 = (props) => { + const { + children, + firstDate, + lastDate, + onUnselect, + onSelect, + } = props; + + const [firstSelectedDate, setFirstSelectedDate] = useState( + firstDate + ? getDataDateString(firstDate) + : null, + ); + const [lastSelectedDate, setLastSelectedDate] = useState( + 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 ( + + {children} + + ); +}; + +export default DatePickerContextProvider; diff --git a/src/widgets/DatePicker/ui/components/CustomDragLayer/index.ts b/src/widgets/DatePicker/ui/components/CustomDragLayer/index.ts new file mode 100644 index 0000000..5aba48e --- /dev/null +++ b/src/widgets/DatePicker/ui/components/CustomDragLayer/index.ts @@ -0,0 +1 @@ +export { default as CustomDragLayer } from './ui/CustomDragLayer.ui'; diff --git a/src/widgets/DatePicker/ui/components/CustomDragLayer/ui/CustomDragLayer.style.module.css b/src/widgets/DatePicker/ui/components/CustomDragLayer/ui/CustomDragLayer.style.module.css new file mode 100644 index 0000000..a37d455 --- /dev/null +++ b/src/widgets/DatePicker/ui/components/CustomDragLayer/ui/CustomDragLayer.style.module.css @@ -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; +} diff --git a/src/widgets/DatePicker/ui/components/CustomDragLayer/ui/CustomDragLayer.ui.tsx b/src/widgets/DatePicker/ui/components/CustomDragLayer/ui/CustomDragLayer.ui.tsx new file mode 100644 index 0000000..467b102 --- /dev/null +++ b/src/widgets/DatePicker/ui/components/CustomDragLayer/ui/CustomDragLayer.ui.tsx @@ -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 ( +
+ ); +}; + +export default CustomDragLayer; diff --git a/src/widgets/DatePicker/ui/components/DraggableCell/index.ts b/src/widgets/DatePicker/ui/components/DraggableCell/index.ts new file mode 100644 index 0000000..adfb8fa --- /dev/null +++ b/src/widgets/DatePicker/ui/components/DraggableCell/index.ts @@ -0,0 +1 @@ +export { default as DraggableCell } from './ui/DraggableCell.ui'; diff --git a/src/widgets/DatePicker/ui/components/DraggableCell/ui/DraggableCell.style.module.css b/src/widgets/DatePicker/ui/components/DraggableCell/ui/DraggableCell.style.module.css new file mode 100644 index 0000000..79294d7 --- /dev/null +++ b/src/widgets/DatePicker/ui/components/DraggableCell/ui/DraggableCell.style.module.css @@ -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; +} diff --git a/src/widgets/DatePicker/ui/components/DraggableCell/ui/DraggableCell.ui.tsx b/src/widgets/DatePicker/ui/components/DraggableCell/ui/DraggableCell.ui.tsx new file mode 100644 index 0000000..a62f7ae --- /dev/null +++ b/src/widgets/DatePicker/ui/components/DraggableCell/ui/DraggableCell.ui.tsx @@ -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 = (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 ( +
+
+ + {date.getDate()} + +
+
+ ); +}; + +export default DraggableCell; diff --git a/yarn.lock b/yarn.lock index 5c1d1b6..819c98c 100644 --- a/yarn.lock +++ b/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"