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
diff --git a/src/.stories/index.js b/src/.stories/index.js
index 6e407269..f4efb3f2 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,10 +66,27 @@ storiesOf('Higher Order Components', module)
Component={withRange(withKeyboardSupport(Calendar))}
/>
))
+ .add('Multiple Range selection', () => (
+
+ ))
.add('Multiple date selection', () => {
return (
diff --git a/src/Calendar/withMultipleDates.js b/src/Calendar/withMultipleDates.js
index 21ce5d08..2319962d 100644
--- a/src/Calendar/withMultipleDates.js
+++ b/src/Calendar/withMultipleDates.js
@@ -58,10 +58,10 @@ 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) {
const selectedMap = selected.map(date => 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..30244653
--- /dev/null
+++ b/src/Calendar/withMultipleRanges.js
@@ -0,0 +1,195 @@
+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/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_TYPES = {
+ 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('displayIndex', 'setDisplayIndex', 0),
+ 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, setDisplayIndex, displayIndex, ...props}) => ({
+ /* eslint-disable sort-keys */
+ passThrough: {
+ ...passThrough,
+ Day: {
+ onClick: (date) => handleSelect(date, {selected, setDisplayIndex, ...props}),
+ handlers: {
+ onMouseOver: !isTouchDevice && props.selectionStart
+ ? (e) => handleMouseOver(e, {selected, ...props})
+ : null,
+ },
+ },
+ Years: {
+ selected: selected[displayIndex] && parse(selected[displayIndex][displayKey]),
+ onSelect: (date, e, callback) => handleYearSelect(date, callback),
+ },
+ Header: {
+ setDisplayIndex,
+ displayIndex,
+ 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, 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
+
+ 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
+ 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
+ ...getSortedSelection({
+ start: selectionStart,
+ end: date,
+ }),
+ };
+ updateSelectedState(positionOfDate.index, null, null, selected, funcs);
+ }
+ } else { //starting new date range
+ 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, { eventType: selectStart ? EVENT_TYPES.START : EVENT_TYPES.END, modifiedDateIndex: displayIdx });
+ 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);
+
+ if (!date) { return; }
+ if (selectionStartIdx === null) { return; }
+
+ selected[selectionStartIdx] = {
+ eventType: EVENT_TYPES.HOVER,
+ ...getSortedSelection({
+ start: selectionStart,
+ end: date,
+ }),
+ };
+ onSelect(selected);
+}
+
+function handleYearSelect(date, callback) {
+ callback(parse(date));
+}
+
+function getInitialDate({selected, initialSelectedDate}) { //add
+ return initialSelectedDate || 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/Calendar/withRange.js b/src/Calendar/withRange.js
index 7bd0c6f3..e0771f63 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: {
@@ -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') {
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 b8fac0de..2c82c73a 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, EVENT_TYPES} from './Calendar/withMultipleRanges';
/*
* By default, Calendar is a controlled component.
@@ -27,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)});
}