From 7c02c274eaa44bd823d31689862bef6cf0291a22 Mon Sep 17 00:00:00 2001 From: akaidc2 Date: Thu, 16 Nov 2017 16:26:49 -0500 Subject: [PATCH 1/5] Extended withRange to withMultipleRange. Now you can have multiple ranges just like with multipleDate Click anywhere on a range to edit, while editing click on the start date to remove. Still requires keyboard support and header cleanup. (will combine current multi-date and range functionality into one) --- src/.stories/index.js | 13 ++ src/Calendar/withMultipleDates.js | 2 +- src/Calendar/withMultipleRanges.js | 198 +++++++++++++++++++++++++++++ src/index.js | 1 + 4 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 src/Calendar/withMultipleRanges.js diff --git a/src/.stories/index.js b/src/.stories/index.js index 6e407269..a1809cd9 100644 --- a/src/.stories/index.js +++ b/src/.stories/index.js @@ -8,6 +8,7 @@ import InfiniteCalendar, { withKeyboardSupport, withMultipleDates, withRange, + withMultipleRanges, } from '../'; import styles from './stories.scss'; @@ -65,6 +66,18 @@ storiesOf('Higher Order Components', module) Component={withRange(withKeyboardSupport(Calendar))} /> )) + .add('Multiple Range selection', () => ( + + )) .add('Multiple date selection', () => { return ( format(date, 'YYYY-MM-DD')); const index = selectedMap.indexOf(format(date, 'YYYY-MM-DD')); diff --git a/src/Calendar/withMultipleRanges.js b/src/Calendar/withMultipleRanges.js new file mode 100644 index 00000000..2c9c705b --- /dev/null +++ b/src/Calendar/withMultipleRanges.js @@ -0,0 +1,198 @@ +import {compose, withProps, withPropsOnChange, withState} from 'recompose'; +import classNames from 'classnames'; +import {withDefaultProps} from './'; +import {sanitizeDate, withImmutableProps} from '../utils'; +import isBefore from 'date-fns/is_before'; +import enhanceHeader from '../Header/withRange'; +import format from 'date-fns/format'; +import parse from 'date-fns/parse'; +import styles from '../Day/Day.scss'; + +let isTouchDevice = false; + +export const EVENT_TYPE = { + DELETE: 4, + END: 3, + HOVER: 2, + START: 1, +}; + +const PositionTypes = { + START: 'START', + RANGE: 'RANGE', + END: 'END', +}; + +// Enhance Day component to display selected state based on an array of selected dates +export const enhanceDay = withPropsOnChange(['selected'], ({date, selected, theme}) => { + const positionOfDate = determineIfDateAlreadySelected(date, selected); + const isSelected = !!positionOfDate.value; + const isStart = positionOfDate.value === PositionTypes.START; + const isEnd = positionOfDate.value === PositionTypes.END; + const isRange = !(isStart && isEnd); + + const style = isRange && ( + isStart && {backgroundColor: theme.accentColor} || + isEnd && {borderColor: theme.accentColor} + ); + + return { + className: isSelected && isRange && classNames(styles.range, { + [styles.start]: isStart, + [styles.betweenRange]: !isStart && !isEnd, + [styles.end]: isEnd, + }), + isSelected, + selectionStyle: style, + }; +}); + +// Enhancer to handle selecting and displaying multiple dates +export const withMultipleRanges = compose( + withDefaultProps, + withState('scrollDate', 'setScrollDate', getInitialDate), + withState('displayKey', 'setDisplayKey', getInitialDate), + withState('selectionStart', 'setSelectionStart', null), + withState('selectionStartIdx', 'setSelectionStartIdx', null), + withImmutableProps(({ + DayComponent, + HeaderComponent, + YearsComponent, + }) => ({ + DayComponent: enhanceDay(DayComponent), + HeaderComponent: enhanceHeader(HeaderComponent), + })), + withProps(({displayKey, passThrough, selected, setDisplayKey, ...props}) => ({ + /* eslint-disable sort-keys */ + passThrough: { + ...passThrough, + Day: { + onClick: (date) => handleSelect(date, {selected, ...props}), + handlers: { + onMouseOver: !isTouchDevice && props.selectionStart + ? (e) => handleMouseOver(e, {selected, ...props}) + : null, + }, + }, + Years: { + selected: selected && selected[displayKey], + onSelect: (date) => handleYearSelect(date, {displayKey, selected, ...props}), + }, + Header: { + onYearClick: (date, e, key) => setDisplayKey(key || 'start'), + }, + }, + selected: selected + .map(dateObj => { + return { + start: format(dateObj.start, 'YYYY-MM-DD'), + end: format(dateObj.end, 'YYYY-MM-DD'), + }; + }), + })), +); + +function getSortedSelection({start, end}) { + return isBefore(start, end) + ? {start, end} + : {start: end, end: start}; +} + +function handleSelect(date, {onSelect, selected, selectionStart, setSelectionStart, selectionStartIdx, setSelectionStartIdx}) { + const positionOfDate = determineIfDateAlreadySelected(date, selected); + + if(positionOfDate.value && !selectionStart) { //selecting an already defined range + const selectedDate = selected[positionOfDate.index];//not clone so modding this is modding selected + selectedDate.end = date; //not possible to have start/end reversed when clicking on already set range + selectedDate.eventType = EVENT_TYPE.START; + + onSelect(selected); + setSelectionStart(selectedDate.start); + setSelectionStartIdx(positionOfDate.index);//grab index of selected and set in state + } else if (selectionStart) { //ending new date range + if (positionOfDate.value === PositionTypes.START && !(date < selectionStart)) { //if in process and selecting start, assume they want to cancel + selected[selectionStartIdx].eventType = EVENT_TYPE.DELETE + onSelect(selected); //call twice to notify parent component something is about to be deleted + onSelect([...selected.slice(0, positionOfDate.index), ...selected.slice(positionOfDate.index+1)]); + } else { + selected[selectionStartIdx] = { //modifying passed in object without clone due to immediate set state + eventType: EVENT_TYPE.END, + ...getSortedSelection({ + start: selectionStart, + end: date, + }), + }; + onSelect(selected); + } + setSelectionStart(null); + setSelectionStartIdx(null); + } else { //starting new date range + onSelect(selected.concat({eventType:EVENT_TYPE.START, start: date, end: date})); + + setSelectionStart(date); + setSelectionStartIdx(selected.length);//accounts for increase due to concat + } +} + +function handleMouseOver(e, {onSelect, selectionStart, selectionStartIdx, selected}) { + const dateStr = e.target.getAttribute('data-date'); + const date = dateStr && parse(dateStr); + + if (!date) { return; } + if (selectionStartIdx === null) { return; } + + selected[selectionStartIdx] = { + eventType: EVENT_TYPE.HOVER, + ...getSortedSelection({ + start: selectionStart, + end: date, + }), + }; + onSelect(selected); +} + +function handleYearSelect(date, {displayKey, onSelect, selected, setScrollDate}) { + + setScrollDate(date); + onSelect(getSortedSelection( + Object.assign({}, selected, {[displayKey]: parse(date)})) + ); +} + +function getInitialDate({selected}) { //add + return selected && selected.length && selected[0].start || new Date(); +} + +if (typeof window !== 'undefined') { + window.addEventListener('touchstart', function onTouch() { + isTouchDevice = true; + + window.removeEventListener('touchstart', onTouch, false); + }); +} + +function determineIfDateAlreadySelected(date, selected) { + let returnVal ={ + index: -1, + value: '', + }; + selected.forEach((dateObj, idx) => { + if (date < dateObj.start || date > dateObj.end ) return; + if (format(date, 'YYYY-MM-DD') === format(dateObj.start, 'YYYY-MM-DD')) { + returnVal.value = PositionTypes.START; + returnVal.index = idx; + return; + } + if (format(date, 'YYYY-MM-DD') === format(dateObj.end, 'YYYY-MM-DD')) { + returnVal.value = PositionTypes.END; + returnVal.index = idx; + return; + } + if (!returnVal.value) { + returnVal.value = PositionTypes.RANGE; + returnVal.index = idx; + return; + } + }); + return returnVal; +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index b8fac0de..ceba5317 100644 --- a/src/index.js +++ b/src/index.js @@ -7,6 +7,7 @@ export {withDateSelection} from './Calendar/withDateSelection'; export {withKeyboardSupport} from './Calendar/withKeyboardSupport'; export {withMultipleDates, defaultMultipleDateInterpolation} from './Calendar/withMultipleDates'; export {withRange, EVENT_TYPE} from './Calendar/withRange'; +export {withMultipleRanges} from './Calendar/withMultipleRanges'; /* * By default, Calendar is a controlled component. From c104d28980dda87c6d94a486632e2dcee03c593a Mon Sep 17 00:00:00 2001 From: akaidc2 Date: Thu, 16 Nov 2017 17:19:30 -0500 Subject: [PATCH 2/5] x --- .idea/codeStyleSettings.xml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .idea/codeStyleSettings.xml diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml new file mode 100644 index 00000000..f8ec6c97 --- /dev/null +++ b/.idea/codeStyleSettings.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file From 8f02af9e9f586f8481a504fa01df8649cfc4cb6d Mon Sep 17 00:00:00 2001 From: akaidc2 Date: Thu, 16 Nov 2017 22:15:13 -0500 Subject: [PATCH 3/5] display index updates to most currently modified date within range array header click events work correctly now based upon multidate and withRange --- src/Calendar/withMultipleRanges.js | 57 ++++++++++++-------------- src/Calendar/withRange.js | 2 +- src/Header/defaultSelectionRenderer.js | 1 + src/Header/withMultipleRanges.js | 45 ++++++++++++++++++++ src/index.js | 2 +- 5 files changed, 75 insertions(+), 32 deletions(-) create mode 100644 src/Header/withMultipleRanges.js diff --git a/src/Calendar/withMultipleRanges.js b/src/Calendar/withMultipleRanges.js index 2c9c705b..bb6ba91d 100644 --- a/src/Calendar/withMultipleRanges.js +++ b/src/Calendar/withMultipleRanges.js @@ -3,14 +3,14 @@ import classNames from 'classnames'; import {withDefaultProps} from './'; import {sanitizeDate, withImmutableProps} from '../utils'; import isBefore from 'date-fns/is_before'; -import enhanceHeader from '../Header/withRange'; +import enhanceHeader from '../Header/withMultipleRanges'; import format from 'date-fns/format'; import parse from 'date-fns/parse'; import styles from '../Day/Day.scss'; let isTouchDevice = false; -export const EVENT_TYPE = { +export const EVENT_TYPES = { DELETE: 4, END: 3, HOVER: 2, @@ -50,6 +50,7 @@ export const enhanceDay = withPropsOnChange(['selected'], ({date, selected, them // Enhancer to handle selecting and displaying multiple dates export const withMultipleRanges = compose( withDefaultProps, + withState('displayIndex', 'setDisplayIndex', 0), withState('scrollDate', 'setScrollDate', getInitialDate), withState('displayKey', 'setDisplayKey', getInitialDate), withState('selectionStart', 'setSelectionStart', null), @@ -62,12 +63,12 @@ export const withMultipleRanges = compose( DayComponent: enhanceDay(DayComponent), HeaderComponent: enhanceHeader(HeaderComponent), })), - withProps(({displayKey, passThrough, selected, setDisplayKey, ...props}) => ({ + withProps(({displayKey, passThrough, selected, setDisplayKey, setDisplayIndex, displayIndex, ...props}) => ({ /* eslint-disable sort-keys */ passThrough: { ...passThrough, Day: { - onClick: (date) => handleSelect(date, {selected, ...props}), + onClick: (date) => handleSelect(date, {selected, setDisplayIndex, ...props}), handlers: { onMouseOver: !isTouchDevice && props.selectionStart ? (e) => handleMouseOver(e, {selected, ...props}) @@ -75,10 +76,12 @@ export const withMultipleRanges = compose( }, }, Years: { - selected: selected && selected[displayKey], - onSelect: (date) => handleYearSelect(date, {displayKey, selected, ...props}), + selected: selected[displayIndex] && parse(selected[displayIndex][displayKey]), + onSelect: (date, e, callback) => handleYearSelect(date, callback), }, Header: { + setDisplayIndex, + displayIndex, onYearClick: (date, e, key) => setDisplayKey(key || 'start'), }, }, @@ -98,42 +101,40 @@ function getSortedSelection({start, end}) { : {start: end, end: start}; } -function handleSelect(date, {onSelect, selected, selectionStart, setSelectionStart, selectionStartIdx, setSelectionStartIdx}) { +function handleSelect(date, {onSelect, selected, selectionStart, setSelectionStart, selectionStartIdx, setSelectionStartIdx, setDisplayIndex}) { const positionOfDate = determineIfDateAlreadySelected(date, selected); + const funcs = {onSelect, setSelectionStart, setSelectionStartIdx, setDisplayIndex}; if(positionOfDate.value && !selectionStart) { //selecting an already defined range const selectedDate = selected[positionOfDate.index];//not clone so modding this is modding selected selectedDate.end = date; //not possible to have start/end reversed when clicking on already set range - selectedDate.eventType = EVENT_TYPE.START; - onSelect(selected); - setSelectionStart(selectedDate.start); - setSelectionStartIdx(positionOfDate.index);//grab index of selected and set in state - } else if (selectionStart) { //ending new date range + updateSelectedState(positionOfDate.index, selectedDate.start, positionOfDate.index, selected, funcs);//grab index of selected and set in state + } else if (selectionStart) { //ending date range selection if (positionOfDate.value === PositionTypes.START && !(date < selectionStart)) { //if in process and selecting start, assume they want to cancel - selected[selectionStartIdx].eventType = EVENT_TYPE.DELETE - onSelect(selected); //call twice to notify parent component something is about to be deleted - onSelect([...selected.slice(0, positionOfDate.index), ...selected.slice(positionOfDate.index+1)]); + const displayIdx = positionOfDate.index > 0 ? positionOfDate.index -1 : 0; + updateSelectedState(displayIdx, null, null, [...selected.slice(0, positionOfDate.index), ...selected.slice(positionOfDate.index+1)], funcs); } else { selected[selectionStartIdx] = { //modifying passed in object without clone due to immediate set state - eventType: EVENT_TYPE.END, ...getSortedSelection({ start: selectionStart, end: date, }), }; - onSelect(selected); + updateSelectedState(positionOfDate.index, null, null, selected, funcs); } - setSelectionStart(null); - setSelectionStartIdx(null); } else { //starting new date range - onSelect(selected.concat({eventType:EVENT_TYPE.START, start: date, end: date})); - - setSelectionStart(date); - setSelectionStartIdx(selected.length);//accounts for increase due to concat + updateSelectedState(selected.length, date, selected.length, selected.concat({eventType:EVENT_TYPES.START, start: date, end: date}), funcs)//length accounts for increase due to concat } } +function updateSelectedState(displayIdx, selectStart, selectStartIdx, selected, {onSelect, setSelectionStart, setSelectionStartIdx, setDisplayIndex}) { + onSelect(selected); + setDisplayIndex(displayIdx); + setSelectionStart(selectStart); + setSelectionStartIdx(selectStartIdx); +} + function handleMouseOver(e, {onSelect, selectionStart, selectionStartIdx, selected}) { const dateStr = e.target.getAttribute('data-date'); const date = dateStr && parse(dateStr); @@ -142,7 +143,7 @@ function handleMouseOver(e, {onSelect, selectionStart, selectionStartIdx, select if (selectionStartIdx === null) { return; } selected[selectionStartIdx] = { - eventType: EVENT_TYPE.HOVER, + eventType: EVENT_TYPES.HOVER, ...getSortedSelection({ start: selectionStart, end: date, @@ -151,12 +152,8 @@ function handleMouseOver(e, {onSelect, selectionStart, selectionStartIdx, select onSelect(selected); } -function handleYearSelect(date, {displayKey, onSelect, selected, setScrollDate}) { - - setScrollDate(date); - onSelect(getSortedSelection( - Object.assign({}, selected, {[displayKey]: parse(date)})) - ); +function handleYearSelect(date, callback) { + callback(parse(date)); } function getInitialDate({selected}) { //add diff --git a/src/Calendar/withRange.js b/src/Calendar/withRange.js index 7bd0c6f3..b207a22b 100644 --- a/src/Calendar/withRange.js +++ b/src/Calendar/withRange.js @@ -69,7 +69,7 @@ export const withRange = compose( onSelect: (date) => handleYearSelect(date, {displayKey, selected, ...props}), }, Header: { - onYearClick: (date, e, key) => setDisplayKey(key || 'start'), + onYearClick: (date, e, key) => setDisplayKey( key || 'start'), }, }, selected: { diff --git a/src/Header/defaultSelectionRenderer.js b/src/Header/defaultSelectionRenderer.js index d7810ff2..c2572706 100644 --- a/src/Header/defaultSelectionRenderer.js +++ b/src/Header/defaultSelectionRenderer.js @@ -15,6 +15,7 @@ export default function defaultSelectionRenderer(value, { scrollToDate, setDisplay, shouldAnimate, + idx, }) { const date = parse(value); const values = date && [ diff --git a/src/Header/withMultipleRanges.js b/src/Header/withMultipleRanges.js new file mode 100644 index 00000000..e1eb71e0 --- /dev/null +++ b/src/Header/withMultipleRanges.js @@ -0,0 +1,45 @@ +import React from 'react'; +import {withImmutableProps} from '../utils'; +import defaultSelectionRenderer from './defaultSelectionRenderer'; +import styles from './Header.scss'; +import Slider from './Slider'; + +export default withImmutableProps(({renderSelection}) => ({ + renderSelection: (values, props) => { + if (!values.length || !values[0].start && !values[0].end) { + return null; + } + + const dates = values.sort(function (a, b) { + return a.start > b.start; + }); + const index = props.displayIndex; + + const dateFormat = props.locale && props.locale.headerFormat || 'MMM Do'; + + const dateElements = values.map((value, idx) => { + if (value.start === value.end) { + return defaultSelectionRenderer(value.start, {...props, key: value.start+idx}); + } else { + return ( +
+ {defaultSelectionRenderer(value.start, {...props, dateFormat, idx, key: 'start', shouldAnimate: false})} + {defaultSelectionRenderer(value.end, {...props, dateFormat, idx, key: 'end', shouldAnimate: false})} +
+ ); + } + }); + return ( + + props.setDisplayIndex(chIndex, () => + setTimeout(() => props.scrollToDate(dates[chIndex].start, 0, true), 50) + ) + } + > + {dateElements} + + ); + }, +})); diff --git a/src/index.js b/src/index.js index ceba5317..5c771ba2 100644 --- a/src/index.js +++ b/src/index.js @@ -7,7 +7,7 @@ export {withDateSelection} from './Calendar/withDateSelection'; export {withKeyboardSupport} from './Calendar/withKeyboardSupport'; export {withMultipleDates, defaultMultipleDateInterpolation} from './Calendar/withMultipleDates'; export {withRange, EVENT_TYPE} from './Calendar/withRange'; -export {withMultipleRanges} from './Calendar/withMultipleRanges'; +export {withMultipleRanges, EVENT_TYPES} from './Calendar/withMultipleRanges'; /* * By default, Calendar is a controlled component. From b272f27124174be0d5bc17e60b6dbe903951ece7 Mon Sep 17 00:00:00 2001 From: akaidc2 Date: Fri, 17 Nov 2017 14:10:16 -0500 Subject: [PATCH 4/5] Added in ability for extensions to pass back extra event data when onSelect Called --- src/Calendar/withMultipleRanges.js | 2 +- src/index.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Calendar/withMultipleRanges.js b/src/Calendar/withMultipleRanges.js index bb6ba91d..62cdfcf6 100644 --- a/src/Calendar/withMultipleRanges.js +++ b/src/Calendar/withMultipleRanges.js @@ -129,7 +129,7 @@ function handleSelect(date, {onSelect, selected, selectionStart, setSelectionSta } function updateSelectedState(displayIdx, selectStart, selectStartIdx, selected, {onSelect, setSelectionStart, setSelectionStartIdx, setDisplayIndex}) { - onSelect(selected); + onSelect(selected, { eventType: selectStart ? EVENT_TYPES.START : EVENT_TYPES.END }); setDisplayIndex(displayIdx); setSelectionStart(selectStart); setSelectionStartIdx(selectStartIdx); diff --git a/src/index.js b/src/index.js index 5c771ba2..2c82c73a 100644 --- a/src/index.js +++ b/src/index.js @@ -28,10 +28,10 @@ export default class DefaultCalendar extends Component { this.setState({selected}); } } - handleSelect = (selected) => { + handleSelect = (selected, eventData) => { const {onSelect, interpolateSelection} = this.props; - if (typeof onSelect === 'function') { onSelect(selected); } + if (typeof onSelect === 'function') { onSelect(selected, eventData); } this.setState({selected: interpolateSelection(selected, this.state.selected)}); } From 015e3fcb4a4a2eb68866c8d429dc6119111a809f Mon Sep 17 00:00:00 2001 From: akaidc2 Date: Fri, 17 Nov 2017 17:00:10 -0500 Subject: [PATCH 5/5] Added in initialSelectedDate to all HOC's so that the user can chose what date will be displayed after the calendar gets caught in a render cycle. Also added update index to the eventData object passed back from multipleRange onSelect --- src/.stories/index.js | 9 +++++++-- src/Calendar/withMultipleDates.js | 4 ++-- src/Calendar/withMultipleRanges.js | 6 +++--- src/Calendar/withRange.js | 4 ++-- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/.stories/index.js b/src/.stories/index.js index a1809cd9..f4efb3f2 100644 --- a/src/.stories/index.js +++ b/src/.stories/index.js @@ -69,9 +69,13 @@ storiesOf('Higher Order Components', module) .add('Multiple Range selection', () => ( diff --git a/src/Calendar/withMultipleDates.js b/src/Calendar/withMultipleDates.js index 2e17079b..2319962d 100644 --- a/src/Calendar/withMultipleDates.js +++ b/src/Calendar/withMultipleDates.js @@ -58,8 +58,8 @@ function handleYearSelect(date, callback) { callback(parse(date)); } -function getInitialDate({selected}) { - return selected.length ? selected[0] : new Date(); +function getInitialDate({selected, initialSelectedDate}) { + return selected.length ? initialSelectedDate || selected[0] : new Date(); } //why is this needed when it could all technically be housed within this HOC's handle select function export function defaultMultipleDateInterpolation(date, selected) { diff --git a/src/Calendar/withMultipleRanges.js b/src/Calendar/withMultipleRanges.js index 62cdfcf6..30244653 100644 --- a/src/Calendar/withMultipleRanges.js +++ b/src/Calendar/withMultipleRanges.js @@ -129,7 +129,7 @@ function handleSelect(date, {onSelect, selected, selectionStart, setSelectionSta } function updateSelectedState(displayIdx, selectStart, selectStartIdx, selected, {onSelect, setSelectionStart, setSelectionStartIdx, setDisplayIndex}) { - onSelect(selected, { eventType: selectStart ? EVENT_TYPES.START : EVENT_TYPES.END }); + onSelect(selected, { eventType: selectStart ? EVENT_TYPES.START : EVENT_TYPES.END, modifiedDateIndex: displayIdx }); setDisplayIndex(displayIdx); setSelectionStart(selectStart); setSelectionStartIdx(selectStartIdx); @@ -156,8 +156,8 @@ function handleYearSelect(date, callback) { callback(parse(date)); } -function getInitialDate({selected}) { //add - return selected && selected.length && selected[0].start || new Date(); +function getInitialDate({selected, initialSelectedDate}) { //add + return initialSelectedDate || selected && selected.length && selected[0].start || new Date(); } if (typeof window !== 'undefined') { diff --git a/src/Calendar/withRange.js b/src/Calendar/withRange.js index b207a22b..e0771f63 100644 --- a/src/Calendar/withRange.js +++ b/src/Calendar/withRange.js @@ -124,8 +124,8 @@ function handleYearSelect(date, {displayKey, onSelect, selected, setScrollDate}) ); } -function getInitialDate({selected}) { - return selected && selected.start || new Date(); +function getInitialDate({selected, initialSelectedDate}) { + return initialSelectedDate || selected && selected.start || new Date(); } if (typeof window !== 'undefined') {