diff --git a/CalendarPicker/Controls.js b/CalendarPicker/Controls.js index ab9b9f58..c918003f 100644 --- a/CalendarPicker/Controls.js +++ b/CalendarPicker/Controls.js @@ -1,41 +1,40 @@ -import React from 'react'; -import { - View, - TouchableOpacity, - Text, -} from 'react-native'; -import PropTypes from 'prop-types'; - -export default function Controls(props) { - const { - styles, - textStyles, - label, - component, - onPressControl, - disabled, - } = props; - - return ( - onPressControl()} - style={styles} - disabled={disabled} - hitSlop={{ top: 20, bottom: 20, left: 40, right: 40 }} - > - - { component || - - { label } - - } - - - ); -} - -Controls.propTypes = { - styles: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), - label: PropTypes.string, - onPressControl: PropTypes.func.isRequired, -}; +import React from 'react'; +import { + TouchableOpacity, + Text, +} from 'react-native'; +import PropTypes from 'prop-types'; + +export default function Controls(props) { + const { + styles, + textStyles, + label, + component, + onPressControl, + disabled, + } = props; + + return ( + onPressControl()} + style={styles} + disabled={disabled} + hitSlop={{ top: 20, bottom: 20, left: 40, right: 40 }} + > + { component ? + ( disabled ? null : component ) + : + + { disabled ? null : label } + + } + + ); +} + +Controls.propTypes = { + styles: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), + label: PropTypes.string, + onPressControl: PropTypes.func.isRequired, +}; diff --git a/CalendarPicker/Day.js b/CalendarPicker/Day.js index f79b6183..0cb9fe67 100644 --- a/CalendarPicker/Day.js +++ b/CalendarPicker/Day.js @@ -1,259 +1,218 @@ -import React from 'react'; -import { - View, - Text, - TouchableOpacity -} from 'react-native'; -import PropTypes from 'prop-types'; -import moment from 'moment'; - -export default function Day(props) { - const { - day, - month, - year, - styles, - customDatesStyles, - onPressDay, - selectedStartDate, - selectedEndDate, - allowRangeSelection, - allowBackwardRangeSelect, - selectedDayStyle: propSelectedDayStyle, - selectedDisabledDatesTextStyle, - selectedRangeStartStyle, - selectedRangeStyle, - selectedRangeEndStyle, - textStyle, - todayTextStyle, - selectedDayTextStyle: propSelectedDayTextStyle, - selectedRangeStartTextStyle, - selectedRangeEndTextStyle, - minDate, - maxDate, - disabledDates, - disabledDatesTextStyle, - minRangeDuration, - maxRangeDuration, - enableDateChange - } = props; - - const thisDay = moment({year, month, day, hour: 12 }); - const today = moment(); - - let dateOutOfRange; - let computedSelectedDayStyle = styles.dayButton; // may be overridden depending on state - let selectedDayTextStyle = {}; - let selectedDayStyle; - let overrideOutOfRangeTextStyle; - let dateIsBeforeMin = false; - let dateIsAfterMax = false; - let dateIsDisabled = false; - let dateRangeLessThanMin = false; - let dateRangeGreaterThanMax = false; - - // First let's check if date is out of range - // Check whether props maxDate / minDate are defined. If not supplied, - // don't restrict dates. - if (maxDate) { - dateIsAfterMax = thisDay.isAfter(maxDate, 'day'); - } - if (minDate) { - dateIsBeforeMin = thisDay.isBefore(minDate, 'day'); - } - - if (disabledDates) { - if (Array.isArray(disabledDates) && disabledDates.indexOf(thisDay.valueOf()) >= 0) { - dateIsDisabled = true; - } - else if (disabledDates instanceof Function) { - dateIsDisabled = disabledDates(thisDay); - } - } - - if (allowRangeSelection && selectedStartDate && !selectedEndDate) { - let daysDiff = thisDay.diff(selectedStartDate, 'days'); // may be + or - - daysDiff = allowBackwardRangeSelect ? Math.abs(daysDiff) : daysDiff; - - if (maxRangeDuration) { - if (Array.isArray(maxRangeDuration)) { - let maxRangeEntry = maxRangeDuration.find(mrd => selectedStartDate.isSame(mrd.date, 'day') ); - if (maxRangeEntry && daysDiff > maxRangeEntry.maxDuration) { - dateRangeGreaterThanMax = true; - } - } else if(daysDiff > maxRangeDuration) { - dateRangeGreaterThanMax = true; - } - } - - if (minRangeDuration) { - if (Array.isArray(minRangeDuration)) { - let minRangeEntry = minRangeDuration.find(mrd => selectedStartDate.isSame(mrd.date, 'day') ); - if (minRangeEntry && daysDiff < minRangeEntry.minDuration) { - dateRangeLessThanMin = true; - } - } else if(daysDiff < minRangeDuration) { - dateRangeLessThanMin = true; - } - } - - if (!allowBackwardRangeSelect && daysDiff < 0) { - dateRangeLessThanMin = true; - } - } - - dateOutOfRange = dateIsAfterMax || dateIsBeforeMin || dateIsDisabled || dateRangeLessThanMin || dateRangeGreaterThanMax; - - let isThisDaySameAsSelectedStart = thisDay.isSame(selectedStartDate, 'day'); - let isThisDaySameAsSelectedEnd = thisDay.isSame(selectedEndDate, 'day'); - let isThisDateInSelectedRange = - selectedStartDate - && selectedEndDate - && thisDay.isBetween(selectedStartDate, selectedEndDate,'day','[]'); - - // If date is in range let's apply styles - if (!dateOutOfRange || isThisDaySameAsSelectedStart || isThisDaySameAsSelectedEnd || isThisDateInSelectedRange) { - // set today's style - let isToday = thisDay.isSame(today, 'day'); - if (isToday) { - computedSelectedDayStyle = styles.selectedToday; - // todayTextStyle prop overrides selectedDayTextColor (created via makeStyles) - selectedDayTextStyle = [todayTextStyle || styles.selectedDayLabel, propSelectedDayTextStyle]; - } - - const custom = getCustomDateStyle({customDatesStyles, date: thisDay}); - - if (isToday && custom.style) { - // Custom date style overrides 'today' style. It may be reset below - // by date selection styling. - computedSelectedDayStyle = [styles.selectedToday, custom.style]; - } - - // set selected day style - if (!allowRangeSelection && - selectedStartDate && - isThisDaySameAsSelectedStart) - { - computedSelectedDayStyle = styles.selectedDay; - selectedDayTextStyle = [styles.selectedDayLabel, isToday && todayTextStyle, propSelectedDayTextStyle]; - // selectedDayStyle prop overrides selectedDayColor (created via makeStyles) - selectedDayStyle = propSelectedDayStyle || styles.selectedDayBackground; - } - - // Set selected ranges styles - if (allowRangeSelection) { - if (selectedStartDate && selectedEndDate) { - // Apply style for start date - if (isThisDaySameAsSelectedStart) { - computedSelectedDayStyle = [styles.startDayWrapper, selectedRangeStyle, selectedRangeStartStyle]; - selectedDayTextStyle = [styles.selectedDayLabel, propSelectedDayTextStyle, selectedRangeStartTextStyle]; - } - // Apply style for end date - if (isThisDaySameAsSelectedEnd) { - computedSelectedDayStyle = [styles.endDayWrapper, selectedRangeStyle, selectedRangeEndStyle]; - selectedDayTextStyle = [styles.selectedDayLabel, propSelectedDayTextStyle, selectedRangeEndTextStyle]; - } - // Apply style if start date is the same as end date - if (isThisDaySameAsSelectedEnd && - isThisDaySameAsSelectedStart && - selectedEndDate.isSame(selectedStartDate, 'day')) - { - computedSelectedDayStyle = [styles.selectedDay, styles.selectedDayBackground, selectedRangeStyle]; - selectedDayTextStyle = [styles.selectedDayLabel, propSelectedDayTextStyle, selectedRangeStartTextStyle]; - } - // Apply style for days inside of range, excluding start & end dates. - if (thisDay.isBetween(selectedStartDate, selectedEndDate, 'day', '()')) { - computedSelectedDayStyle = [styles.inRangeDay, selectedRangeStyle]; - selectedDayTextStyle = [styles.selectedDayLabel, propSelectedDayTextStyle]; - } - } - // Apply style if start date has been selected but end date has not - if (selectedStartDate && - !selectedEndDate && - isThisDaySameAsSelectedStart) - { - computedSelectedDayStyle = [styles.startDayWrapper, selectedRangeStyle, selectedRangeStartStyle]; - selectedDayTextStyle = [styles.selectedDayLabel, propSelectedDayTextStyle, selectedRangeStartTextStyle]; - // Override out of range start day text style when minRangeDuration = 1. - // This allows selected start date's text to be styled by selectedRangeStartTextStyle - // even when it's below minRangeDuration. - overrideOutOfRangeTextStyle = selectedRangeStartTextStyle; - } - } - - if (dateOutOfRange) { // start or end date selected, and this date outside of range. - return ( - - - - { day } - - - - ); - } else { - return ( - - onPressDay({year, month, day}) }> - - { day } - - - - ); - } - } - else { // dateOutOfRange = true, and no selected start or end date. - const custom = getCustomDateStyle({customDatesStyles, date: thisDay}); - // Allow customDatesStyles to override disabled dates if allowDisabled set - if (!custom.allowDisabled) { - custom.containerStyle = null; - custom.style = null; - custom.textStyle = null; - } - return ( - - - - { day } - - - - ); - } -} - -function getCustomDateStyle({customDatesStyles, date}) { - if (Array.isArray(customDatesStyles)) { - for (let cds of customDatesStyles) { - if (date.isSame(moment(cds.date), 'day')) { - return {...cds}; - } - } - } - else if (customDatesStyles instanceof Function) { - let cds = customDatesStyles(date) || {}; - return {...cds}; - } - return {}; -} - -Day.defaultProps = { - customDatesStyles: [], -}; - -Day.propTypes = { - styles: PropTypes.shape({}), - day: PropTypes.number, - onPressDay: PropTypes.func, - disabledDates: PropTypes.oneOfType([PropTypes.array, PropTypes.func]), - minRangeDuration: PropTypes.oneOfType([PropTypes.array, PropTypes.number]), - maxRangeDuration: PropTypes.oneOfType([PropTypes.array, PropTypes.number]), -}; +import React from 'react'; +import { + View, + Text, + TouchableOpacity +} from 'react-native'; +import PropTypes from 'prop-types'; +import moment from 'moment'; + +export default function Day(props) { + const { + day, + month, + year, + styles, + customDatesStyles, + onPressDay, + selectedStartDate, + selectedEndDate, + allowRangeSelection, + allowBackwardRangeSelect, + selectedDayStyle, + selectedRangeStartStyle, + selectedRangeStyle, + selectedRangeEndStyle, + textStyle, + todayTextStyle, + minDate, + maxDate, + disabledDates, + disabledDatesTextStyle, + minRangeDuration, + maxRangeDuration, + enableDateChange + } = props; + + const thisDay = moment({year, month, day, hour: 12 }); + const today = moment(); + + let dateOutOfRange; + let daySelectedStyle = styles.dayButton; // may be overridden depending on state + let selectedDayColorStyle = {}; + let propSelectedDayStyle; + let dateIsBeforeMin = false; + let dateIsAfterMax = false; + let dateIsDisabled = false; + let dateRangeLessThanMin = false; + let dateRangeGreaterThanMax = false; + let customContainerStyle, customDateStyle, customTextStyle; + + // First let's check if date is out of range + // Check whether props maxDate / minDate are defined. If not supplied, + // don't restrict dates. + if (maxDate) { + dateIsAfterMax = thisDay.isAfter(maxDate, 'day'); + } + if (minDate) { + dateIsBeforeMin = thisDay.isBefore(minDate, 'day'); + } + + if (disabledDates) { + if (Array.isArray(disabledDates) && disabledDates.indexOf(thisDay.valueOf()) >= 0) { + dateIsDisabled = true; + } + else if (disabledDates instanceof Function) { + dateIsDisabled = disabledDates(thisDay); + } + } + + if (allowRangeSelection && selectedStartDate && !selectedEndDate) { + let daysDiff = thisDay.diff(selectedStartDate, 'days'); // may be + or - + daysDiff = allowBackwardRangeSelect ? Math.abs(daysDiff) : daysDiff; + + if (maxRangeDuration) { + if (Array.isArray(maxRangeDuration)) { + let maxRangeEntry = maxRangeDuration.find(mrd => selectedStartDate.isSame(mrd.date, 'day') ); + if (maxRangeEntry && daysDiff > maxRangeEntry.maxDuration) { + dateRangeGreaterThanMax = true; + } + } else if(daysDiff > maxRangeDuration) { + dateRangeGreaterThanMax = true; + } + } + + if (minRangeDuration) { + if (Array.isArray(minRangeDuration)) { + let minRangeEntry = minRangeDuration.find(mrd => selectedStartDate.isSame(mrd.date, 'day') ); + if (minRangeEntry && daysDiff < minRangeEntry.minDuration && daysDiff !== 0) { + dateRangeLessThanMin = true; + } + } else if(daysDiff < minRangeDuration && daysDiff !== 0) { + dateRangeLessThanMin = true; + } + } + + if (!allowBackwardRangeSelect && daysDiff < 0) { + dateRangeLessThanMin = true; + } + } + + dateOutOfRange = dateIsAfterMax || dateIsBeforeMin || dateIsDisabled || dateRangeLessThanMin || dateRangeGreaterThanMax; + + // If date is in range let's apply styles + if (!dateOutOfRange) { + // set today's style + let isToday = thisDay.isSame(today, 'day'); + if (isToday) { + daySelectedStyle = styles.selectedToday; + // todayTextStyle prop overrides selectedDayTextColor (created via makeStyles) + selectedDayColorStyle = todayTextStyle || styles.selectedDayLabel; + } + + if (Array.isArray(customDatesStyles)) { + for (let cds of customDatesStyles) { + if (thisDay.isSame(moment(cds.date), 'day')) { + customContainerStyle = cds.containerStyle; + customDateStyle = cds.style; + customTextStyle = cds.textStyle; + break; + } + } + } + else if (customDatesStyles instanceof Function) { + let cds = customDatesStyles(thisDay) || {}; + customContainerStyle = cds.containerStyle; + customDateStyle = cds.style; + customTextStyle = cds.textStyle; + } + if (isToday && customDateStyle) { + // Custom date style overrides 'today' style. It may be reset below + // by date selection styling. + daySelectedStyle = [daySelectedStyle, customDateStyle]; + } + + let isThisDaySameAsSelectedStart = thisDay.isSame(selectedStartDate, 'day'); + let isThisDaySameAsSelectedEnd = thisDay.isSame(selectedEndDate, 'day'); + + // set selected day style + if (!allowRangeSelection && + selectedStartDate && + isThisDaySameAsSelectedStart) { + daySelectedStyle = styles.selectedDay; + selectedDayColorStyle = [styles.selectedDayLabel, isToday && todayTextStyle]; + // selectedDayStyle prop overrides selectedDayColor (created via makeStyles) + propSelectedDayStyle = selectedDayStyle || styles.selectedDayBackground; + } + + // Set selected ranges styles + if (allowRangeSelection) { + if (selectedStartDate && selectedEndDate) { + // Apply style for start date + if (isThisDaySameAsSelectedStart) { + daySelectedStyle = [styles.startDayWrapper, selectedRangeStyle, selectedRangeStartStyle]; + selectedDayColorStyle = styles.selectedDayLabel; + } + // Apply style for end date + if (isThisDaySameAsSelectedEnd) { + daySelectedStyle = [styles.endDayWrapper, selectedRangeStyle, selectedRangeEndStyle]; + selectedDayColorStyle = styles.selectedDayLabel; + } + // Apply style if start date is the same as end date + if (isThisDaySameAsSelectedEnd && + isThisDaySameAsSelectedStart && + selectedEndDate.isSame(selectedStartDate, 'day')) { + daySelectedStyle = [styles.selectedDay, styles.selectedDayBackground, selectedRangeStyle]; + selectedDayColorStyle = styles.selectedDayLabel; + } + // Apply style if this day is in range + if (thisDay.isBetween(selectedStartDate, selectedEndDate, 'day')) { + daySelectedStyle = [styles.inRangeDay, selectedRangeStyle]; + selectedDayColorStyle = styles.selectedDayLabel; + } + } + // Apply style if start date has been selected but end date has not + if (selectedStartDate && + !selectedEndDate && + isThisDaySameAsSelectedStart) { + daySelectedStyle = [styles.startDayWrapper, selectedRangeStyle, selectedRangeStartStyle]; + selectedDayColorStyle = styles.selectedDayLabel; + } + } + + return ( + + onPressDay(day) }> + + { day } + + + + ); + } + else { // dateOutOfRange = true + return ( + + + { day } + + + ); + } +} + +Day.defaultProps = { + customDatesStyles: [], +}; + +Day.propTypes = { + styles: PropTypes.shape({}), + day: PropTypes.number, + onPressDay: PropTypes.func, + disabledDates: PropTypes.oneOfType([PropTypes.array, PropTypes.func]), + minRangeDuration: PropTypes.oneOfType([PropTypes.array, PropTypes.number]), + maxRangeDuration: PropTypes.oneOfType([PropTypes.array, PropTypes.number]), +}; diff --git a/CalendarPicker/DaysGridView.js b/CalendarPicker/DaysGridView.js index 7222334a..738a335f 100644 --- a/CalendarPicker/DaysGridView.js +++ b/CalendarPicker/DaysGridView.js @@ -1,271 +1,208 @@ -import React, { Component } from 'react'; -import { View } from 'react-native'; -import PropTypes from 'prop-types'; -import { stylePropType } from './localPropTypes'; -import Day from './Day'; -import EmptyDay from './EmptyDay'; -import { Utils } from './Utils'; -import moment from 'moment'; - -export default class DaysGridView extends Component { - constructor(props) { - super(props); - - this.initMonthSettings = props => { - const { - month, - year, - showDayStragglers, - firstDay = 0, - } = props; - - // Retrieve total days in this month & year, accounting for leap years. - const numDaysInMonth = Utils.getDaysInMonth(month, year); - - // Calculate days in prev month for day stragglers. - let prevMonth, prevMonthYear; - let numDaysInPrevMonth; - if (showDayStragglers) { - prevMonth = month - 1; - prevMonthYear = year; - if (prevMonth < 0) { - prevMonth = 11; - prevMonthYear--; - } - numDaysInPrevMonth = Utils.getDaysInMonth(prevMonth, prevMonthYear); - } - - // Create a date for day one of the current given month and year - const firstDayOfMonth = moment({ year, month, day: 1 }); - - // Determine which day of the week day 1 falls on. - // See https://github.com/stephy/CalendarPicker/issues/49 - // isoWeekday() gets the ISO day of the week with 1=Monday and 7=Sunday. - const firstWeekDay = firstDayOfMonth.isoWeekday(); - - // Determine starting index based on first day of week prop. - const startIndex = (firstDay > 0) ? (firstWeekDay + Utils.FIRST_DAY_OFFSETS[firstDay]) % 7 : firstWeekDay; - - return { - maxWeekRows: 6, - numDaysInWeek: 7, - numDaysInMonth, - numDaysInPrevMonth, - firstDayOfMonth, - firstWeekDay, - startIndex, - }; - }; - - const monthSettings = this.initMonthSettings(props); - this.state = { - monthSettings, - daysGrid: this.generateDaysGrid(monthSettings), - }; - } - - componentDidUpdate(prevProps) { - // Optimize re-renders by checking props, with special handling for selected dates. - // Shallow compare prop changes, excluding selected dates. - const propDiffs = Utils.shallowDiff(this.props, prevProps, ['selectedStartDate', 'selectedEndDate']); - if (propDiffs.length) { - // Recreate days - const monthSettings = this.initMonthSettings(this.props); - this.setState({ - monthSettings, - daysGrid: this.generateDaysGrid(monthSettings), - }); - } - else { - // Update daysGrid entries when selected date(s) affect this month. - const { selectedStartDate, selectedEndDate } = this.props; - const { selectedStartDate: prevSelStart, selectedEndDate: prevSelEnd } = prevProps; - const { firstDayOfMonth } = this.state.monthSettings; - const isSelectedDiff = - !Utils.compareDates(selectedStartDate, prevSelStart, 'day') || - !Utils.compareDates(selectedEndDate, prevSelEnd, 'day'); - // Check that selected date(s) match this month. - if (isSelectedDiff && ( - Utils.compareDates(selectedStartDate, firstDayOfMonth, 'month') || - Utils.compareDates(selectedEndDate, firstDayOfMonth, 'month') || - Utils.compareDates(prevSelStart, firstDayOfMonth, 'month') || - Utils.compareDates(prevSelEnd, firstDayOfMonth, 'month') )) - { - // Range selection potentially affects all dates in the month. Recreate. - if (this.props.allowRangeSelection) { - this.setState({ - daysGrid: this.generateDaysGrid(this.state.monthSettings), - }); - } - else { - // Search for affected dates and modify those only - const daysGrid = [...this.state.daysGrid]; - const { year } = this.props; - for (let i = 0; i - ), - }); - } - - renderEmptyDay(key) { - return ({ - component: ( - - ), - }); - } - - renderDayStraggler({key, day}) { - return ({ - day, - // month doesn't matter for stragglers as long as isn't set to current month - component: ( - true} - disabledDatesTextStyle={this.props.disabledDatesTextStyle} - textStyle={this.props.textStyle} - /> - ) - }); - } - - // Create grid of days. - generateDaysGrid = params => { - const { - numDaysInWeek, - maxWeekRows, - startIndex, - numDaysInMonth, - numDaysInPrevMonth - } = params; - let daysGrid = [[]]; - let dayOfMonth = 1; - let dayNextMonth = 1; - let lastFilledRow = 0; - - // Week rows - for (let i = 0; i < maxWeekRows; i++) { - daysGrid[i] = []; - // Days in week - for (let j = 0; j < numDaysInWeek; j++) { - if (i === 0) { - // first row: start current month's day on the correct weekday - if (j >= startIndex) { - if (dayOfMonth <= numDaysInMonth) { - daysGrid[i].push(this.renderDayInCurrentMonth(dayOfMonth++)); - } - } else { - const key = '' + i + j; - daysGrid[i].push(this.props.showDayStragglers ? - // Show previous month's days - this.renderDayStraggler({ - key, - day: numDaysInPrevMonth - startIndex + j + 1, - }) - : - //... otherwise blank - this.renderEmptyDay(key) - ); - } - } else { - if (dayOfMonth <= numDaysInMonth) { - lastFilledRow = i; - daysGrid[i].push(this.renderDayInCurrentMonth(dayOfMonth++)); - } - else { - if (this.props.showDayStragglers && i <= lastFilledRow) { - // Show next month's days - daysGrid[i].push(this.renderDayStraggler({ - key: '' + i + j, - day: dayNextMonth++, - })); - } - } - } - } - } - return daysGrid; - } - - render() { - const { styles } = this.props; - const { daysGrid } = this.state; - const renderedDaysGrid = daysGrid.map((weekRow, i) => ( - - { weekRow.map(day => day.component ) } - - )); - - return ( - - { renderedDaysGrid } - - ); - } -} - -DaysGridView.propTypes = { - styles: stylePropType, - month: PropTypes.number.isRequired, - year: PropTypes.number.isRequired, - onPressDay: PropTypes.func, - firstDay: PropTypes.number, - selectedDayStyle: stylePropType, - selectedRangeStartStyle: stylePropType, - selectedRangeStyle: stylePropType, - selectedRangeEndStyle: stylePropType, - todayTextStyle: stylePropType, - selectedDayTextStyle: stylePropType, - customDatesStyles: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.arrayOf(PropTypes.shape({ - date: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.instanceOf(Date), - PropTypes.instanceOf(moment) - ]), - containerStyle: stylePropType, - style: stylePropType, - textStyle: stylePropType, - })), - ]), - disabledDates: PropTypes.oneOfType([PropTypes.array, PropTypes.func]), - disabledDatesTextStyle: stylePropType, - minRangeDuration: PropTypes.oneOfType([PropTypes.array, PropTypes.number]), - maxRangeDuration: PropTypes.oneOfType([PropTypes.array, PropTypes.number]), -}; +import React from 'react'; +import uuid from 'uuid/v4'; +import { + View, + Text, + ViewPropTypes as RNViewPropTypes, +} from 'react-native'; +import PropTypes from 'prop-types'; +import Day from './Day'; +import EmptyDay from './EmptyDay'; +import { Utils } from './Utils'; +import moment from 'moment'; + +const ViewPropTypes = RNViewPropTypes || View.propTypes; + +export default function DaysGridView(props) { + const { + month, + year, + styles, + onPressDay, + startFromMonday, + selectedStartDate, + selectedEndDate, + allowRangeSelection, + allowBackwardRangeSelect, + textStyle, + todayTextStyle, + selectedDayStyle, + selectedRangeStartStyle, + selectedRangeStyle, + selectedRangeEndStyle, + customDatesStyles, + minDate, + maxDate, + disabledDates, + disabledDatesTextStyle, + minRangeDuration, + maxRangeDuration, + enableDateChange, + showDayStragglers, + } = props; + + // let's get the total of days in this month, we need the year as well, since + // leap years have different amount of days in February + const totalDays = Utils.getDaysInMonth(month, year); + + // Calculate days in prev month for day stragglers. + let totalDaysPrevMonth, prevMonth, prevMonthYear, dayNextMonth; + if (showDayStragglers) { + prevMonth = month - 1; + prevMonthYear = year; + if (prevMonth < 0) { + prevMonth = 11; + prevMonthYear--; + } + totalDaysPrevMonth = Utils.getDaysInMonth(prevMonth, prevMonthYear); + // Next month's day always starts at 1 and never overflows + dayNextMonth = 1; + } + + // Let's create a date for day one of the current given month and year + const firstDayOfMonth = moment({ year, month, day: 1 }); + + // isoWeekday() gets the ISO day of the week with 1 being Monday and 7 being Sunday. + // We will need this to know what day of the week to show day 1 + // See https://github.com/stephy/CalendarPicker/issues/49 + const firstWeekDay = firstDayOfMonth.isoWeekday(); + + // fill up an array of days with the amount of days in the current month + const days = Array.apply(null, {length: totalDays}).map(Number.call, Number); + + // 7 days in a week. + const dayArray = [ 0, 1, 2, 3, 4, 5, 6 ]; + + // There can be 4 to 6 rows of weeks in a month. + const weekArray = [ 0, 1, 2, 3, 4, 5 ]; + + // Get the starting index, based upon whether we are using monday or sunday as first day. + const startIndex = (startFromMonday ? firstWeekDay - 1 : firstWeekDay) % 7; + + function renderDayInCurrentMonth() { + const day = days.shift() + 1; + return ( + + ); + } + + function renderDayStraggler({key, day}) { + return ( + true} + disabledDatesTextStyle={disabledDatesTextStyle} + textStyle={textStyle} + /> + ); + } + + + function generateDatesForWeek(i) { + let lastFilledRow = 0; + return dayArray.map(dayIndex => { + if (i === 0) { + // first row: start current month's day on the correct weekday + if (dayIndex >= startIndex) { + if (days.length > 0) { + return renderDayInCurrentMonth(); + } + } else { + return showDayStragglers ? + // Show previous month's days + renderDayStraggler({ + key: '' + i + dayIndex, + day: totalDaysPrevMonth - startIndex + dayIndex + 1, + }) + : + ( //... otherwise blank + + ); + } + } else { + if (days.length > 0) { + lastFilledRow = i; + return renderDayInCurrentMonth(); + } + else if (showDayStragglers && i <= lastFilledRow) { + // Show next month's days + return renderDayStraggler({ + key: '' + i + dayIndex, + day: dayNextMonth++, + }); + } + } + }); + } + + return ( + + { weekArray.map(weekIndexOfMonth => ( + + { generateDatesForWeek(weekIndexOfMonth) } + + )) + } + + ); +} + +DaysGridView.propTypes = { + styles: PropTypes.shape(), + month: PropTypes.number.isRequired, + year: PropTypes.number.isRequired, + onPressDay: PropTypes.func, + startFromMonday: PropTypes.bool, + selectedDayStyle: ViewPropTypes.style, + selectedRangeStartStyle: ViewPropTypes.style, + selectedRangeStyle: ViewPropTypes.style, + selectedRangeEndStyle: ViewPropTypes.style, + todayTextStyle: Text.propTypes.style, + customDatesStyles: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.arrayOf(PropTypes.shape({ + date: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.instanceOf(Date), + PropTypes.instanceOf(moment) + ]), + containerStyle: ViewPropTypes.style, + style: ViewPropTypes.style, + textStyle: Text.propTypes.style, + })), + ]), + disabledDates: PropTypes.oneOfType([PropTypes.array, PropTypes.func]), + disabledDatesTextStyle: Text.propTypes.style, + minRangeDuration: PropTypes.oneOfType([PropTypes.array, PropTypes.number]), + maxRangeDuration: PropTypes.oneOfType([PropTypes.array, PropTypes.number]), +}; diff --git a/CalendarPicker/EmptyDay.js b/CalendarPicker/EmptyDay.js index c91e6e58..86823b59 100644 --- a/CalendarPicker/EmptyDay.js +++ b/CalendarPicker/EmptyDay.js @@ -1,16 +1,16 @@ -import React from 'react'; -import { View } from 'react-native'; -import PropTypes from 'prop-types'; - -export default function EmptyDay(props) { - const { styles } = props; - return( - - - - ); -} - -EmptyDay.propTypes = { - styles: PropTypes.shape({}) -}; +import React from 'react'; +import { View } from 'react-native'; +import PropTypes from 'prop-types'; + +export default function EmptyDay(props) { + const { styles } = props; + return( + + + + ); +} + +EmptyDay.propTypes = { + styles: PropTypes.shape({}) +}; diff --git a/CalendarPicker/HeaderControls.js b/CalendarPicker/HeaderControls.js index 8abdffd1..70ade8aa 100644 --- a/CalendarPicker/HeaderControls.js +++ b/CalendarPicker/HeaderControls.js @@ -1,91 +1,89 @@ -import React from 'react'; -import { - View, - Text, - Platform, - TouchableOpacity -} from 'react-native'; -import PropTypes from 'prop-types'; -import { Utils } from './Utils'; -import Controls from './Controls'; - -export default function HeaderControls(props) { - const { - styles, - currentMonth, - currentYear, - onPressNext, - onPressPrevious, - onPressMonth, - onPressYear, - months, - previousComponent, - nextComponent, - previousTitle, - nextTitle, - previousTitleStyle, - nextTitleStyle, - monthTitleStyle, - yearTitleStyle, - textStyle, - restrictMonthNavigation, - maxDate, - minDate, - headingLevel, - monthYearHeaderWrapperStyle, - headerWrapperStyle - } = props; - const MONTHS = months || Utils.MONTHS; // English Month Array - const monthName = MONTHS[currentMonth]; - const year = currentYear; - - const disablePreviousMonth = restrictMonthNavigation && Utils.isSameMonthAndYear(minDate, currentMonth, currentYear); - const disableNextMonth = restrictMonthNavigation && Utils.isSameMonthAndYear(maxDate, currentMonth, currentYear); - - const accessibilityProps = { accessibilityRole: 'header' }; - if (Platform.OS === 'web') { - accessibilityProps['aria-level'] = headingLevel; - } - - return ( - - - - - - { monthName } - - - - - { year } - - - - - - ); -} - -HeaderControls.propTypes = { - currentMonth: PropTypes.number, - currentYear: PropTypes.number, - onPressNext: PropTypes.func, - onPressPrevious: PropTypes.func, - onPressMonth: PropTypes.func, - onPressYear: PropTypes.func, -}; +import React from 'react'; +import { + View, + Text, + Platform, + TouchableOpacity +} from 'react-native'; +import PropTypes from 'prop-types'; +import { Utils } from './Utils'; +import Controls from './Controls'; + +export default function HeaderControls(props) { + const { + lang, + styles, + currentMonth, + currentYear, + onPressNext, + onPressPrevious, + onPressMonth, + onPressYear, + months, + previousComponent, + nextComponent, + previousTitle, + nextTitle, + previousTitleStyle, + nextTitleStyle, + textStyle, + restrictMonthNavigation, + maxDate, + minDate, + headingLevel, + monthYearHeaderWrapperStyle, + } = props; + const MONTHS = months || lang == 'ar' ? Utils.ARABIC_MONTHS : Utils.MONTHS; // English Month Array + const monthName = MONTHS[currentMonth]; + const year = currentYear; + + const disablePreviousMonth = restrictMonthNavigation && Utils.isSameMonthAndYear(minDate, currentMonth, currentYear); + const disableNextMonth = restrictMonthNavigation && Utils.isSameMonthAndYear(maxDate, currentMonth, currentYear); + + const accessibilityProps = { accessibilityRole: 'header' }; + if (Platform.OS === 'web') { + accessibilityProps['aria-level'] = headingLevel; + } + + return ( + + + + + + {monthName} + + + + + {year} + + + + + + ); +} + +HeaderControls.propTypes = { + currentMonth: PropTypes.number, + currentYear: PropTypes.number, + onPressNext: PropTypes.func, + onPressPrevious: PropTypes.func, + onPressMonth: PropTypes.func, + onPressYear: PropTypes.func, +}; diff --git a/CalendarPicker/LICENSE.txt b/CalendarPicker/LICENSE.txt index 786d4bfe..9f061a61 100644 --- a/CalendarPicker/LICENSE.txt +++ b/CalendarPicker/LICENSE.txt @@ -1,5 +1,5 @@ -The MIT License (MIT) -Copyright 2016 Yahoo Inc. -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The MIT License (MIT) +Copyright 2016 Yahoo Inc. +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/CalendarPicker/Month.js b/CalendarPicker/Month.js index b5d1b290..6f08f819 100644 --- a/CalendarPicker/Month.js +++ b/CalendarPicker/Month.js @@ -1,76 +1,77 @@ -import React from 'react'; -import { - View, - Text, - TouchableOpacity -} from 'react-native'; -import PropTypes from 'prop-types'; -import { Utils } from './Utils'; - -export default function Month(props) { - const { - months, - currentMonth: month, - currentYear: year, - styles, - onSelectMonth, - textStyle, - minDate, - maxDate, - } = props; - - const MONTHS = months || Utils.MONTHS; // English Month Array - const monthName = MONTHS[month]; - - let monthOutOfRange; - let monthIsBeforeMin = false; - let monthIsAfterMax = false; - let monthIsDisabled = false; - - // Check whether month is outside of min/max range. - if (maxDate && (maxDate.year() === year)) { - monthIsAfterMax = month > maxDate.month(); - } - if (minDate && (minDate.year() === year)) { - monthIsBeforeMin = month < minDate.month(); - } - - // ToDo: disabledMonths props to disable months separate from disabledDates - - monthOutOfRange = monthIsAfterMax || monthIsBeforeMin || monthIsDisabled; - - const onSelect = () => { - let _year = year; - if (minDate && (year < minDate.year())) { - _year = minDate.year(); - } - if (maxDate && (year > maxDate.year())) { - _year = maxDate.year(); - } - onSelectMonth({month, year: _year}); - }; - - return ( - - { !monthOutOfRange ? - - - { monthName } - - - : - - { monthName } - - } - - ); -} - -Month.propTypes = { - styles: PropTypes.shape({}), - currentMonth: PropTypes.number, - currentYear: PropTypes.number, - onSelectMonth: PropTypes.func, -}; +import React from 'react'; +import { + View, + Text, + TouchableOpacity +} from 'react-native'; +import PropTypes from 'prop-types'; +import { Utils } from './Utils'; + +export default function Month(props) { + const { + lang, + months, + currentMonth: month, + currentYear: year, + styles, + onSelectMonth, + textStyle, + minDate, + maxDate, + } = props; + console.log(lang) + const MONTHS = months || lang == 'ar' ? Utils.ARABIC_MONTHS : Utils.MONTHS; // English Month Array + const monthName = MONTHS[month]; + + let monthOutOfRange; + let monthIsBeforeMin = false; + let monthIsAfterMax = false; + let monthIsDisabled = false; + + // Check whether month is outside of min/max range. + if (maxDate) { + monthIsAfterMax = month > maxDate.month(); + } + if (minDate) { + monthIsBeforeMin = month < minDate.month(); + } + + // ToDo: disabledMonths props to disable months separate from disabledDates + + monthOutOfRange = monthIsAfterMax || monthIsBeforeMin || monthIsDisabled; + + const onSelect = () => { + let _year = year; + if (minDate && (year < minDate.year())) { + _year = minDate.year(); + } + if (maxDate && (year > maxDate.year())) { + _year = maxDate.year(); + } + onSelectMonth({ month, year: _year }); + }; + + return ( + + {!monthOutOfRange ? + + + {monthName} + + + : + + {monthName} + + } + + ); +} + +Month.propTypes = { + styles: PropTypes.shape({}), + currentMonth: PropTypes.number, + currentYear: PropTypes.number, + onSelectMonth: PropTypes.func, +}; diff --git a/CalendarPicker/MonthSelector.js b/CalendarPicker/MonthSelector.js index 6d46e768..257ac34f 100644 --- a/CalendarPicker/MonthSelector.js +++ b/CalendarPicker/MonthSelector.js @@ -1,49 +1,51 @@ -// Parent view for Month selector - -import React, { Component } from 'react'; -import { View } from 'react-native'; -import MonthsGridView from './MonthsGridView'; -import MonthsHeader from './MonthsHeader'; - -export default class MonthSelector extends Component { - constructor(props) { - super(props); - this.state = { - currentYear: props.currentYear, - }; - } - - render() { - const { - styles, - textStyle, - title, - headingLevel, - currentYear, - months, - minDate, - maxDate, - onSelectMonth, - } = this.props; - - return ( - - - - - ); - } -} +// Parent view for Month selector + +import React, { Component } from 'react'; +import { View } from 'react-native'; +import MonthsGridView from './MonthsGridView'; +import MonthsHeader from './MonthsHeader'; + +export default class MonthSelector extends Component { + constructor(props) { + super(props); + this.state = { + currentYear: props.currentYear, + }; + } + + render() { + const { + lang, + styles, + textStyle, + title, + headingLevel, + currentYear, + months, + minDate, + maxDate, + onSelectMonth, + } = this.props; + + return ( + + + + + ); + } +} diff --git a/CalendarPicker/MonthsGridView.js b/CalendarPicker/MonthsGridView.js index 8d513bc6..2cf73f08 100644 --- a/CalendarPicker/MonthsGridView.js +++ b/CalendarPicker/MonthsGridView.js @@ -1,58 +1,60 @@ -import React from 'react'; -import { View } from 'react-native'; -import PropTypes from 'prop-types'; -import Month from './Month'; - - -export default function MonthsGridView(props) { - const { - currentYear, - months, - styles, - onSelectMonth, - textStyle, - minDate, - maxDate, - } = props; - const _months = Array.from(Array(12).keys()); - const columnArray = [ 0, 1, 2 ]; - const rowArray = [ 0, 1, 2, 3 ]; - - function generateColumns() { - const column = columnArray.map(index => { - const currentMonth = _months.shift(); - return ( - - ); - }); - return column; - } - - return ( - - { rowArray.map(index => ( - - { generateColumns() } - - )) - } - - ); -} - -MonthsGridView.propTypes = { - styles: PropTypes.shape(), - currentYear: PropTypes.number.isRequired, - months: PropTypes.array, - onSelectMonth: PropTypes.func, -}; +import React from 'react'; +import { View } from 'react-native'; +import PropTypes from 'prop-types'; +import Month from './Month'; + + +export default function MonthsGridView(props) { + const { + lang, + currentYear, + months, + styles, + onSelectMonth, + textStyle, + minDate, + maxDate, + } = props; + const _months = Array.from(Array(12).keys()); + const columnArray = [0, 1, 2]; + const rowArray = [0, 1, 2, 3]; + + function generateColumns() { + const column = columnArray.map(index => { + const currentMonth = _months.shift(); + return ( + + ); + }); + return column; + } + + return ( + + {rowArray.map(index => ( + + {generateColumns()} + + )) + } + + ); +} + +MonthsGridView.propTypes = { + styles: PropTypes.shape(), + currentYear: PropTypes.number.isRequired, + months: PropTypes.array, + onSelectMonth: PropTypes.func, +}; diff --git a/CalendarPicker/MonthsHeader.js b/CalendarPicker/MonthsHeader.js index 15899493..0e3fad0e 100644 --- a/CalendarPicker/MonthsHeader.js +++ b/CalendarPicker/MonthsHeader.js @@ -1,36 +1,35 @@ -import React from 'react'; -import { - View, - Text, - Platform, -} from 'react-native'; -import PropTypes from 'prop-types'; -import { stylePropType } from './localPropTypes'; - -export default function MonthsHeader(props) { - const { - styles, - textStyle, - headingLevel, - title, - } = props; - - const accessibilityProps = { accessibilityRole: 'header' }; - if (Platform.OS === 'web') { - accessibilityProps['aria-level'] = headingLevel; - } - - return ( - - - { title } - - - ); -} - -MonthsHeader.propTypes = { - styles: stylePropType, - textStyle: stylePropType, - title: PropTypes.string -}; +import React from 'react'; +import { + View, + Text, + Platform, +} from 'react-native'; +import PropTypes from 'prop-types'; + +export default function MonthsHeader(props) { + const { + styles, + textStyle, + headingLevel, + title, + } = props; + + const accessibilityProps = { accessibilityRole: 'header' }; + if (Platform.OS === 'web') { + accessibilityProps['aria-level'] = headingLevel; + } + + return ( + + + { title } + + + ); +} + +MonthsHeader.propTypes = { + styles: PropTypes.shape(), + textStyle: Text.propTypes.style, + title: PropTypes.string +}; diff --git a/CalendarPicker/Scroller.js b/CalendarPicker/Scroller.js deleted file mode 100644 index ce515266..00000000 --- a/CalendarPicker/Scroller.js +++ /dev/null @@ -1,284 +0,0 @@ -// This is a bi-directional infinite scroller. -// As the beginning & end are reached, the dates are recalculated and the current -// index adjusted to match the previous visible date. -// RecyclerListView helps to efficiently recycle instances, but the data that -// it's fed is finite. Hence the data must be shifted at the ends to appear as -// an infinite scroller. - -import React, { Component } from 'react'; -import { View, Platform } from 'react-native'; -import PropTypes from 'prop-types'; -import { RecyclerListView, DataProvider, LayoutProvider } from 'recyclerlistview'; -import moment from 'moment'; - -export default class CalendarScroller extends Component { - static propTypes = { - data: PropTypes.array.isRequired, - initialRenderIndex: PropTypes.number, - renderMonth: PropTypes.func, - renderMonthParams: PropTypes.object.isRequired, - minDate: PropTypes.any, - maxDate: PropTypes.any, - maxSimultaneousMonths: PropTypes.number, - horizontal: PropTypes.bool, - updateMonthYear: PropTypes.func, - onMonthChange: PropTypes.func, - } - - static defaultProps = { - data: [], - renderMonthParams: { styles: {} }, - }; - - constructor(props) { - super(props); - - this.updateLayout = dims => { - const itemWidth = dims.containerWidth; - let itemHeight = dims.containerHeight; - if (dims.dayWrapper && dims.dayWrapper.height) { - itemHeight = dims.dayWrapper.height * 6; // max 6 row weeks per month - } - - const layoutProvider = new LayoutProvider( - () => 0, // only 1 view type - (type, dim) => { - dim.width = itemWidth; - dim.height = itemHeight; - } - ); - - return { layoutProvider, itemHeight, itemWidth }; - }; - - this.dataProvider = new DataProvider((r1, r2) => { - return r1 !== r2; - }); - - this.updateMonthsData = data => { - return { - data, - numMonths: data.length, - dataProvider: this.dataProvider.cloneWithRows(data), - }; - }; - - this.state = { - ...this.updateLayout(props.renderMonthParams.styles), - ...this.updateMonthsData(props.data), - numVisibleItems: 1, // updated in onLayout - }; - } - - shouldComponentUpdate(prevProps, prevState) { - return this.state.data !== prevState.data || - this.state.itemHeight !== prevState.itemHeight || - this.state.itemWidth !== prevState.itemWidth || - this.props.renderMonthParams !== prevProps.renderMonthParams; - } - - componentDidUpdate(prevProps) { - let newState = {}; - let updateState = false; - - if (this.props.renderMonthParams.styles !== prevProps.renderMonthParams.styles) { - updateState = true; - newState = this.updateLayout(this.props.renderMonthParams.styles); - } - - if (this.props.data !== prevProps.data) { - updateState = true; - newState = {...newState, ...this.updateMonthsData(this.props.data)}; - } - - if (Platform.OS === 'android' && - this.props.renderMonthParams.selectedStartDate !== prevProps.renderMonthParams.selectedStartDate) - { - // Android unexpectedly jumps to previous month on first selected date. - // Scroll RLV to selected date's month. - this.goToDate(this.props.renderMonthParams.selectedStartDate, 100); - } - - if (updateState) { - this.setState(newState); - } - } - - goToDate = (date, delay) => { - const data = this.state.data; - for (let i = 0; i < data.length; i++) { - if (data[i].isSame(date, 'month')) { - if (delay) { - setTimeout(() => this.rlv && this.rlv.scrollToIndex(i, false), delay); - } - else { - this.rlv && this.rlv.scrollToIndex(i, false); - } - break; - } - } - } - - // Scroll left, guarding against start index. - scrollLeft = () => { - const { currentIndex, numVisibleItems } = this.state; - if (currentIndex === 0) { - return; - } - const newIndex = Math.max(currentIndex - numVisibleItems, 0); - this.rlv && this.rlv.scrollToIndex(newIndex, true); - } - - // Scroll right, guarding against end index. - scrollRight = () => { - const { currentIndex, numVisibleItems, numMonths } = this.state; - const newIndex = Math.min(currentIndex + numVisibleItems, numMonths - 1); - this.rlv && this.rlv.scrollToIndex(newIndex, true); - } - - // Shift dates when end of list is reached. - shiftMonthsForward = currentMonth => { - this.shiftMonths(currentMonth, this.state.numMonths / 3); - } - - // Shift dates when beginning of list is reached. - shiftMonthsBackward = currentMonth => { - this.shiftMonths(currentMonth, this.state.numMonths * 2/3); - } - - shiftMonths = (currentMonth, offset) => { - const prevVisMonth = currentMonth.clone(); - const newStartMonth = prevVisMonth.clone().subtract(Math.floor(offset), 'months'); - this.updateMonths(prevVisMonth, newStartMonth); - } - - updateMonths = (prevVisMonth, newStartMonth) => { - if (this.shifting) { - return; - } - const { - minDate, - maxDate, - restrictMonthNavigation, - } = this.props; - const data = []; - let _newStartMonth = newStartMonth; - if (minDate && restrictMonthNavigation && newStartMonth.isBefore(minDate, 'month')) { - _newStartMonth = moment(minDate); - } - for (let i = 0; i < this.state.numMonths; i++) { - let date = _newStartMonth.clone().add(i, 'months'); - if (maxDate && restrictMonthNavigation && date.isAfter(maxDate, 'month')) { - break; - } - data.push(date); - } - // Prevent reducing range when the minDate - maxDate range is small. - if (data.length < this.props.maxSimultaneousMonths) { - return; - } - - // Scroll to previous date - for (let i = 0; i < data.length; i++) { - if (data[i].isSame(prevVisMonth, 'month')) { - this.shifting = true; - this.rlv && this.rlv.scrollToIndex(i, false); - // RecyclerListView sometimes returns position to old index after - // moving to the new one. Set position again after delay. - setTimeout(() => { - this.rlv && this.rlv.scrollToIndex(i, false); - this.shifting = false; // debounce - }, 800); - break; - } - } - this.setState({ - data, - dataProvider: this.dataProvider.cloneWithRows(data), - }); - } - - // Track which dates are visible. - onVisibleIndicesChanged = (all, now) => { - const { - data, - numMonths, - currentMonth: _currentMonth, - } = this.state; - - const { - updateMonthYear, - onMonthChange, - } = this.props; - - // "now" contains the inflight indices, whereas "all" reflects indices - // after scrolling has settled. Prioritize "now" for faster header updates. - const currentIndex = now[0] || all[0]; - const currentMonth = data[currentIndex]; // a Moment date - - // Fire month/year update on month changes. This is - // necessary for the header and onMonthChange updates. - if (!_currentMonth || !_currentMonth.isSame(currentMonth, 'month')) { - const currMonth = currentMonth && currentMonth.clone(); - onMonthChange && onMonthChange(currMonth); - } - - updateMonthYear && updateMonthYear(currentMonth, true); - - if (currentIndex === 0) { - this.shiftMonthsBackward(currentMonth); - } else if (currentIndex > numMonths - 3) { - this.shiftMonthsForward(currentMonth); - } - this.setState({ - currentMonth, - currentIndex, - }); - } - - onLayout = event => { - const containerWidth = event.nativeEvent.layout.width; - this.setState({ - numVisibleItems: Math.floor(containerWidth / this.state.itemWidth), - ...this.updateLayout(this.props.renderMonthParams.styles), - }); - } - - rowRenderer = (type, rowMonth, i, extState) => { - const { updateMonthYear, renderMonth } = this.props; - const { currentMonth: month, currentYear: year } = updateMonthYear(rowMonth); - return renderMonth && renderMonth({...extState, month, year}); - } - - render() { - const { - data, - numMonths, - itemHeight: height, - itemWidth: width, - layoutProvider, - dataProvider, - } = this.state; - if (!data || numMonths === 0 || !height) { - return null; - } - return ( - - this.rlv = rlv} - layoutProvider={layoutProvider} - dataProvider={dataProvider} - rowRenderer={this.rowRenderer} - extendedState={this.props.renderMonthParams} - initialRenderIndex={this.props.initialRenderIndex} - onVisibleIndicesChanged={this.onVisibleIndicesChanged} - isHorizontal={this.props.horizontal} - scrollViewProps={{ - showsHorizontalScrollIndicator: false, - snapToInterval: this.props.horizontal ? width : height, - }} - /> - - ); - } -} diff --git a/CalendarPicker/Swiper.js b/CalendarPicker/Swiper.js new file mode 100644 index 00000000..4ca74feb --- /dev/null +++ b/CalendarPicker/Swiper.js @@ -0,0 +1,107 @@ +import React, { Component } from 'react'; +import { + PanResponder, + View, +} from 'react-native'; + +export const swipeDirections = { + SWIPE_UP: 'SWIPE_UP', + SWIPE_DOWN: 'SWIPE_DOWN', + SWIPE_LEFT: 'SWIPE_LEFT', + SWIPE_RIGHT: 'SWIPE_RIGHT' +}; + +const swipeConfig = { + velocityThreshold: 0.3, + directionalOffsetThreshold: 80 +}; + +function isValidSwipe(velocity, velocityThreshold, directionalOffset, directionalOffsetThreshold) { + return Math.abs(velocity) > velocityThreshold && Math.abs(directionalOffset) < directionalOffsetThreshold; +} + +export default class Swiper extends Component { + constructor(props, context) { + super(props, context); + this.swipeConfig = Object.assign(swipeConfig, props.config); + + const responderEnd = this._handlePanResponderEnd.bind(this); + const shouldSetResponder = this._handleShouldSetPanResponder.bind(this); + this._panResponder = PanResponder.create({ //stop JS beautify collapse + onStartShouldSetPanResponder: shouldSetResponder, + onMoveShouldSetPanResponder: shouldSetResponder, + onPanResponderRelease: responderEnd, + onPanResponderTerminate: responderEnd + }); + } + + componentDidUpdate(prevProps) { + if (prevProps.config !== this.props.config) { + this.swipeConfig = Object.assign(swipeConfig, this.props.config); + } + } + + _handleShouldSetPanResponder(evt, gestureState) { + return evt.nativeEvent.touches.length === 1 && !this._gestureIsClick(gestureState); + } + + _gestureIsClick(gestureState) { + return Math.abs(gestureState.dx) < 5 && Math.abs(gestureState.dy) < 5; + } + + _handlePanResponderEnd(evt, gestureState) { + const swipeDirection = this._getSwipeDirection(gestureState); + this._triggerSwipeHandlers(swipeDirection, gestureState); + } + + _triggerSwipeHandlers(swipeDirection, gestureState) { + const {onSwipe, onSwipeUp, onSwipeDown, onSwipeLeft, onSwipeRight} = this.props; + const {SWIPE_LEFT, SWIPE_RIGHT, SWIPE_UP, SWIPE_DOWN} = swipeDirections; + onSwipe && onSwipe(swipeDirection, gestureState); + switch (swipeDirection) { + case SWIPE_LEFT: + onSwipeLeft && onSwipeLeft(gestureState); + break; + case SWIPE_RIGHT: + onSwipeRight && onSwipeRight(gestureState); + break; + case SWIPE_UP: + onSwipeUp && onSwipeUp(gestureState); + break; + case SWIPE_DOWN: + onSwipeDown && onSwipeDown(gestureState); + break; + } + } + + _getSwipeDirection(gestureState) { + const {SWIPE_LEFT, SWIPE_RIGHT, SWIPE_UP, SWIPE_DOWN} = swipeDirections; + const {dx, dy} = gestureState; + if (this._isValidHorizontalSwipe(gestureState)) { + return (dx > 0) + ? SWIPE_RIGHT + : SWIPE_LEFT; + } else if (this._isValidVerticalSwipe(gestureState)) { + return (dy > 0) + ? SWIPE_DOWN + : SWIPE_UP; + } + return null; + } + + _isValidHorizontalSwipe(gestureState) { + const {vx, dy} = gestureState; + const {velocityThreshold, directionalOffsetThreshold} = this.swipeConfig; + return isValidSwipe(vx, velocityThreshold, dy, directionalOffsetThreshold); + } + + _isValidVerticalSwipe(gestureState) { + const {vy, dx} = gestureState; + const {velocityThreshold, directionalOffsetThreshold} = this.swipeConfig; + return isValidSwipe(vy, velocityThreshold, dx, directionalOffsetThreshold); + } + + render() { + return (); + } +} diff --git a/CalendarPicker/Utils.js b/CalendarPicker/Utils.js index e6358621..4227fda5 100644 --- a/CalendarPicker/Utils.js +++ b/CalendarPicker/Utils.js @@ -1,70 +1,29 @@ -/** - * Calendar Picker Component - * - * Copyright 2016 Yahoo Inc. - * Licensed under the terms of the MIT license. See LICENSE file in the project root for terms. - */ - -export const Utils = { - START_DATE: 'START_DATE', - END_DATE: 'END_DATE', - WEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], - MONTHS: [ - 'January', 'February', 'March', 'April', 'May', 'June', 'July', - 'August', 'September', 'October', 'November', 'December' - ], - MAX_ROWS: 7, - MAX_COLUMNS: 7, - FIRST_DAY_OFFSETS: [0, -1, 5, 4, 3, 2, 1], - getDaysInMonth: function(month, year) { - const lastDayOfMonth = new Date(year, month + 1, 0); - return lastDayOfMonth.getDate(); - }, - isSameMonthAndYear: function(date, month, year) { - if (date) { - return date.month() === month && date.year() === year; - } - return false; - }, - // Test whether objects' values are different. - // `exclusions` param ignores provided keys. - // Returns array of keys that are different (empty array means identical). - shallowDiff: function(a, b, exclusions = []) { - const diffs = []; - for (let key of Object.keys(a)) { - if (exclusions.includes(key)) { - continue; - } - if (a[key] !== b[key]) { - diffs.push(key); - } - } - return diffs; - }, - // Robust compare Moment dates. - compareDates: function(a, b, granularity) { - // Allow for falsy (null & undefined) equality. - if (!a && !b) { - return true; - } - return !!a && !!b && a.isSame(b, granularity); - }, - getWeekdays: function(firstDay = 0) { - let from = firstDay; - const weekdays = []; - for (let i = 0; i < Utils.WEEKDAYS.length; i++) { - weekdays.push(Utils.WEEKDAYS[from]); - from = from >= Utils.WEEKDAYS.length - 1 ? 0 : from + 1; - } - return weekdays; - }, - getISOWeekdaysOrder: function(firstDay = 0) { - let from = firstDay === 0 ? 7 : firstDay; - const order = []; - for (let i = 0; i < Utils.WEEKDAYS.length; i++) { - order.push(from); - from = from >= Utils.WEEKDAYS.length ? 1 : from + 1; - } - return order; - }, -}; +/** + * Calendar Picker Component + * + * Copyright 2016 Yahoo Inc. + * Licensed under the terms of the MIT license. See LICENSE file in the project root for terms. + */ + +export const Utils = { + START_DATE: 'START_DATE', + END_DATE: 'END_DATE', + ENGLISH_WEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + ARABIC_WEEKDAYS: ['الاحد', 'الاتنين', 'الثلاثاء', 'الاربعاء', 'الخميس', 'الجمعة', 'السبت'], + ENGLISH_WEEKDAYS_MON: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], + ARABIC_WEEKDAYS_MON: ['الاتنين', 'الثلاثاء', 'الاربعاء', 'الخميس', 'الجمعة', 'السبت', 'الاحد',], + MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], + ARABIC_MONTHS: ['يناير', 'فبراير', 'مارس', 'ابريل', 'مايو', 'يونيو', 'يوليو', 'اغسطس', 'سبتمبر', 'اكتوبر', 'نوفمبر', 'ديسمبر',], + MAX_ROWS: 7, + MAX_COLUMNS: 7, + getDaysInMonth: function (month, year) { + const lastDayOfMonth = new Date(year, month + 1, 0); + return lastDayOfMonth.getDate(); + }, + isSameMonthAndYear: function (date, month, year) { + if (date) { + return date.month() === month && date.year() === year; + } + return false; + }, +}; diff --git a/CalendarPicker/Weekdays.js b/CalendarPicker/Weekdays.js index eae6101c..493412a8 100644 --- a/CalendarPicker/Weekdays.js +++ b/CalendarPicker/Weekdays.js @@ -1,55 +1,83 @@ -import React from 'react'; -import { - View, - Text, -} from 'react-native'; -import PropTypes from 'prop-types'; -import { Utils } from './Utils'; - -export default function Weekdays(props) { - const { - styles, - firstDay, - currentMonth: month, - currentYear: year, - weekdays, - textStyle, - dayLabelsWrapper, - customDayHeaderStyles, - } = props; - - // dayOfWeekNums: ISO week day numbers - const dayOfWeekNums = Utils.getISOWeekdaysOrder(firstDay); - let wd = weekdays; - if (!wd) { - wd = firstDay ? Utils.getWeekdays(firstDay) : Utils.WEEKDAYS; // English Week days Array - } - - return ( - - { wd.map((day, key) => { - const dayOfWeekTextStyle = [styles.dayLabels, textStyle]; - let customDayOfWeekStyles = {}; - if (customDayHeaderStyles instanceof Function) { - const dayOfWeek = dayOfWeekNums[key]; - customDayOfWeekStyles = customDayHeaderStyles({dayOfWeek, month, year}) || {}; - dayOfWeekTextStyle.push(customDayOfWeekStyles.textStyle); - } - return ( - - - {day} - - - ); - }) - } - - ); -} - -Weekdays.propTypes = { - firstDay: PropTypes.number, - weekdays: PropTypes.array, - customDayHeaderStyles: PropTypes.func, -}; +import React from 'react'; +import { + View, + Text, +} from 'react-native'; +import PropTypes from 'prop-types'; +import { Utils } from './Utils'; +import { locale } from 'moment'; + +export default function Weekdays(props) { + const { + styles, + startFromMonday, + currentMonth: month, + currentYear: year, + weekdays, + textStyle, + dayLabelsWrapper, + customDayHeaderStyles, + dayOfWeekStyles, // ToDo: Deprecated. Remove. + lang + } = props; + + // dayOfWeekNums: ISO week day numbers + const dayOfWeekNums = startFromMonday ? [1, 2, 3, 4, 5, 6, 7] : [7, 1, 2, 3, 4, 5, 6]; + let wd = weekdays; + console.log(wd) + const isArabic = (lang == 'ar') + if (!wd) { + if (isArabic) { + wd = startFromMonday ? Utils.ARABIC_WEEKDAYS_MON : Utils.ARABIC_WEEKDAYS + + } else { + wd = startFromMonday ? Utils.WEEKDAYS_MON : Utils.WEEKDAYS + + } + } + + return ( + + {wd.map((day, key) => { + const dayOfWeekTextStyle = [styles.dayLabels, textStyle]; + let customDayOfWeekStyles = {}; + if (customDayHeaderStyles instanceof Function) { + const dayOfWeek = dayOfWeekNums[key]; + customDayOfWeekStyles = customDayHeaderStyles({ dayOfWeek, month, year }) || {}; + dayOfWeekTextStyle.push(customDayOfWeekStyles.textStyle); + } + // ---------------------------------------------------------------- + // ToDo: Deprecated. Remove + else { + try { + if (dayOfWeekStyles[+key]) { + console.warn('CalendarPicker: dayOfWeekStyles is deprecated. Use customDatesStyles / customDayHeaderStyles callbacks instead.'); + let currentDayStyle = dayOfWeekStyles[+key]; + if (currentDayStyle) { + dayOfWeekTextStyle.push(currentDayStyle); + } + } + } catch (error) { + console.log('Error while updating weekday style: ' + error); + } + } + // ---------------------------------------------------------------- + + return ( + + + {day} + + + ); + }) + } + + ); +} + +Weekdays.propTypes = { + startFromMonday: PropTypes.bool, + weekdays: PropTypes.array, + customDayHeaderStyles: PropTypes.func, +}; diff --git a/CalendarPicker/Year.js b/CalendarPicker/Year.js index e1ed9148..0ea0f03e 100644 --- a/CalendarPicker/Year.js +++ b/CalendarPicker/Year.js @@ -1,74 +1,74 @@ -import React from 'react'; -import { - View, - Text, - TouchableOpacity -} from 'react-native'; -import PropTypes from 'prop-types'; -import moment from 'moment'; - -export default function Year(props) { - const { - year, - currentMonth, - currentYear, - styles, - onSelectYear, - textStyle, - minDate, - maxDate, - } = props; - - let yearOutOfRange; - let yearIsBeforeMin = false; - let yearIsAfterMax = false; - let yearIsDisabled = false; - - // Check whether year is outside of min/max range. - if (maxDate) { - yearIsAfterMax = year > maxDate.year(); - } - if (minDate) { - yearIsBeforeMin = year < minDate.year(); - } - - // ToDo: disabledYears props to disable years separate from disabledDates - - yearOutOfRange = yearIsAfterMax || yearIsBeforeMin || yearIsDisabled; - - const onSelect = () => { - // Guard against navigating to months beyond min/max dates. - let month = currentMonth; - let currentMonthYear = moment({year: currentYear, month}); - if (maxDate && currentMonthYear.isAfter(maxDate, 'month')) { - month = maxDate.month(); - } - if (minDate && currentMonthYear.isBefore(minDate, 'month')) { - month = minDate.month(); - } - onSelectYear({month, year}); - }; - - return ( - - { !yearOutOfRange ? - - - { year } - - - : - - { year } - - } - - ); -} - -Year.propTypes = { - styles: PropTypes.shape({}), - year: PropTypes.number, - onSelectYear: PropTypes.func, -}; +import React from 'react'; +import { + View, + Text, + TouchableOpacity +} from 'react-native'; +import PropTypes from 'prop-types'; +import moment from 'moment'; + +export default function Year(props) { + const { + year, + currentMonth, + currentYear, + styles, + onSelectYear, + textStyle, + minDate, + maxDate, + } = props; + + let yearOutOfRange; + let yearIsBeforeMin = false; + let yearIsAfterMax = false; + let yearIsDisabled = false; + + // Check whether year is outside of min/max range. + if (maxDate) { + yearIsAfterMax = year > maxDate.year(); + } + if (minDate) { + yearIsBeforeMin = year < minDate.year(); + } + + // ToDo: disabledYears props to disable years separate from disabledDates + + yearOutOfRange = yearIsAfterMax || yearIsBeforeMin || yearIsDisabled; + + const onSelect = () => { + // Guard against navigating to months beyond min/max dates. + let month = currentMonth; + let currentMonthYear = moment({year: currentYear, month}); + if (maxDate && currentMonthYear.isAfter(maxDate, 'month')) { + month = maxDate.month(); + } + if (minDate && currentMonthYear.isBefore(minDate, 'month')) { + month = minDate.month(); + } + onSelectYear({month, year}); + }; + + return ( + + { !yearOutOfRange ? + + + { year } + + + : + + { year } + + } + + ); +} + +Year.propTypes = { + styles: PropTypes.shape({}), + year: PropTypes.number, + onSelectYear: PropTypes.func, +}; diff --git a/CalendarPicker/YearSelector.js b/CalendarPicker/YearSelector.js index 57b15489..47976adb 100644 --- a/CalendarPicker/YearSelector.js +++ b/CalendarPicker/YearSelector.js @@ -1,83 +1,83 @@ -// Parent view for Year selector - -import React, { Component } from 'react'; -import { View } from 'react-native'; -import YearsGridView from './YearsGridView'; -import YearsHeader from './YearsHeader'; - -export default class YearSelector extends Component { - constructor(props) { - super(props); - this.state = { - initialYear: props.currentYear, - }; - } - - handleOnYearViewPrevious = () => { - this.setState({ - initialYear: parseInt(Math.max(this.state.initialYear - 25, 0)) - }); - } - - handleOnYearViewNext = () => { - this.setState({ - initialYear: parseInt(this.state.initialYear + 25) - }); - } - - render() { - const { - styles, - textStyle, - title, - initialDate, - currentMonth, - currentYear, - minDate, - maxDate, - restrictNavigation, - previousComponent, - nextComponent, - previousTitle, - nextTitle, - previousTitleStyle, - nextTitleStyle, - headingLevel, - onSelectYear, - } = this.props; - - return ( - - - - - ); - } -} +// Parent view for Year selector + +import React, { Component } from 'react'; +import { View } from 'react-native'; +import YearsGridView from './YearsGridView'; +import YearsHeader from './YearsHeader'; + +export default class YearSelector extends Component { + constructor(props) { + super(props); + this.state = { + initialYear: props.currentYear, + }; + } + + handleOnYearViewPrevious = () => { + this.setState({ + initialYear: parseInt(Math.max(this.state.initialYear - 25, 0)) + }); + } + + handleOnYearViewNext = () => { + this.setState({ + initialYear: parseInt(this.state.initialYear + 25) + }); + } + + render() { + const { + styles, + textStyle, + title, + initialDate, + currentMonth, + currentYear, + minDate, + maxDate, + restrictNavigation, + previousComponent, + nextComponent, + previousTitle, + nextTitle, + previousTitleStyle, + nextTitleStyle, + headingLevel, + onSelectYear, + } = this.props; + + return ( + + + + + ); + } +} diff --git a/CalendarPicker/YearsGridView.js b/CalendarPicker/YearsGridView.js index 9cb61917..428e680a 100644 --- a/CalendarPicker/YearsGridView.js +++ b/CalendarPicker/YearsGridView.js @@ -1,56 +1,56 @@ -import React from 'react'; -import { View } from 'react-native'; -import PropTypes from 'prop-types'; -import Year from './Year'; - - -export default function YearsGridView(props) { - const { - intialYear, - currentMonth, - currentYear, - styles, - onSelectYear, - textStyle, - minDate, - maxDate, - } = props; - const guideArray = [ 0, 1, 2, 3, 4 ]; - let year = intialYear - 13; // center current year in grid - - function generateColumns() { - const column = guideArray.map(() => { - year++; - return ( - - ); - }); - return column; - } - return ( - - { guideArray.map(index => ( - - { generateColumns(index) } - - )) - } - - ); -} - -YearsGridView.propTypes = { - styles: PropTypes.shape(), - intialYear: PropTypes.number.isRequired, - onSelectYear: PropTypes.func, -}; +import React from 'react'; +import { View } from 'react-native'; +import PropTypes from 'prop-types'; +import Year from './Year'; + + +export default function YearsGridView(props) { + const { + intialYear, + currentMonth, + currentYear, + styles, + onSelectYear, + textStyle, + minDate, + maxDate, + } = props; + const guideArray = [ 0, 1, 2, 3, 4 ]; + let year = intialYear - 13; // center current year in grid + + function generateColumns() { + const column = guideArray.map(() => { + year++; + return ( + + ); + }); + return column; + } + return ( + + { guideArray.map(index => ( + + { generateColumns(index) } + + )) + } + + ); +} + +YearsGridView.propTypes = { + styles: PropTypes.shape(), + intialYear: PropTypes.number.isRequired, + onSelectYear: PropTypes.func, +}; diff --git a/CalendarPicker/YearsHeader.js b/CalendarPicker/YearsHeader.js index 7b964b0f..f80ecc9c 100644 --- a/CalendarPicker/YearsHeader.js +++ b/CalendarPicker/YearsHeader.js @@ -1,70 +1,69 @@ -import React from 'react'; -import { - View, - Text, - Platform, -} from 'react-native'; -import PropTypes from 'prop-types'; -import { stylePropType } from './localPropTypes'; -import Controls from './Controls'; - -export default function YearsHeader(props) { - const { - title, - year, - maxDate, - minDate, - restrictNavigation, - styles, - textStyle, - previousComponent, - nextComponent, - previousTitle, - nextTitle, - previousTitleStyle, - nextTitleStyle, - onYearViewPrevious, - onYearViewNext, - headingLevel, - } = props; - - const disablePrevious = restrictNavigation && minDate && (minDate.year() >= year); - const disableNext = restrictNavigation && maxDate && (maxDate.year() <= year); - - const accessibilityProps = { accessibilityRole: 'header' }; - if (Platform.OS === 'web') { - accessibilityProps['aria-level'] = headingLevel; - } - - return ( - - - - { title } - - - - ); -} - -YearsHeader.propTypes = { - styles: stylePropType, - textStyle: stylePropType, - title: PropTypes.string, - onYearViewNext: PropTypes.func, - onYearViewPrevious: PropTypes.func, -}; +import React from 'react'; +import { + View, + Text, + Platform, +} from 'react-native'; +import PropTypes from 'prop-types'; +import Controls from './Controls'; + +export default function YearsHeader(props) { + const { + title, + year, + maxDate, + minDate, + restrictNavigation, + styles, + textStyle, + previousComponent, + nextComponent, + previousTitle, + nextTitle, + previousTitleStyle, + nextTitleStyle, + onYearViewPrevious, + onYearViewNext, + headingLevel, + } = props; + + const disablePrevious = restrictNavigation && (minDate.year() >= year); + const disableNext = restrictNavigation && (maxDate.year() <= year); + + const accessibilityProps = { accessibilityRole: 'header' }; + if (Platform.OS === 'web') { + accessibilityProps['aria-level'] = headingLevel; + } + + return ( + + + + { title } + + + + ); +} + +YearsHeader.propTypes = { + styles: PropTypes.shape(), + textStyle: Text.propTypes.style, + title: PropTypes.string, + onYearViewNext: PropTypes.func, + onYearViewPrevious: PropTypes.func, +}; diff --git a/CalendarPicker/__tests__/CalendarPicker-test.js b/CalendarPicker/__tests__/CalendarPicker-test.js index 4a30e31b..f8a1abfe 100644 --- a/CalendarPicker/__tests__/CalendarPicker-test.js +++ b/CalendarPicker/__tests__/CalendarPicker-test.js @@ -1,50 +1,50 @@ -/* eslint-env jasmine */ - -import React from 'react'; -import renderer from 'react-test-renderer'; -import CalenderPicker from '../index'; - -describe('CalendarPicker', function() { - it('It renders calendar picker', () => { - const CalendarPicker = renderer.create( - - ).toJSON(); - expect(CalendarPicker).toBeTruthy(); - }); - - it('It renders calendar picker with props', () => { - const minDate = new Date(2017, 6, 1); - const maxDate = new Date(2017, 6, 3); - const CalendarPicker = renderer.create( - {}} - /> - ).toJSON(); - expect(CalendarPicker).toBeTruthy(); - }); - - it('It handle selectedStartDate and selectedEndDate props', () => { - const selectedStartDate = new Date(2018, 5, 1); - const selectedEndDate = new Date(2018, 5, 15); - const CalendarPicker = renderer.create( - {}} - /> - ).toJSON(); - expect(CalendarPicker).toBeTruthy(); - }); -}); +/* eslint-env jasmine */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import CalenderPicker from '../index'; + +describe('CalendarPicker', function() { + it('It renders calendar picker', () => { + const CalendarPicker = renderer.create( + + ).toJSON(); + expect(CalendarPicker).toBeTruthy(); + }); + + it('It renders calendar picker with props', () => { + const minDate = new Date(2017, 6, 1); + const maxDate = new Date(2017, 6, 3); + const CalendarPicker = renderer.create( + {}} + /> + ).toJSON(); + expect(CalendarPicker).toBeTruthy(); + }); + + it('It handle selectedStartDate and selectedEndDate props', () => { + const selectedStartDate = new Date(2018, 5, 1); + const selectedEndDate = new Date(2018, 5, 15); + const CalendarPicker = renderer.create( + {}} + /> + ).toJSON(); + expect(CalendarPicker).toBeTruthy(); + }); +}); diff --git a/CalendarPicker/index.js b/CalendarPicker/index.js index 0d30414a..296b128e 100644 --- a/CalendarPicker/index.js +++ b/CalendarPicker/index.js @@ -1,597 +1,601 @@ -import React, { Component } from 'react'; -import { View, Dimensions } from 'react-native'; -import { makeStyles } from './makeStyles'; -import { Utils } from './Utils'; -import HeaderControls from './HeaderControls'; -import Weekdays from './Weekdays'; -import DaysGridView from './DaysGridView'; -import MonthSelector from './MonthSelector'; -import YearSelector from './YearSelector'; -import Scroller from './Scroller'; -import moment from 'moment'; - -export default class CalendarPicker extends Component { - constructor(props) { - super(props); - this.numMonthsScroll = 60; // 5 years - this.state = { - currentMonth: null, - currentYear: null, - currentView: props.initialView || 'days', - selectedStartDate: props.selectedStartDate && moment(props.selectedStartDate), - selectedEndDate: props.selectedEndDate && moment(props.selectedEndDate), - minDate: props.minDate && moment(props.minDate), - maxDate: props.maxDate && moment(props.maxDate), - styles: {}, - ...this.updateScaledStyles(props), - ...this.updateMonthYear(props.initialDate), - ...this.updateDisabledDates(props.disabledDates), - ...this.updateMinMaxRanges(props.minRangeDuration, props.maxRangeDuration), - ...this.createMonths(props, {}), - }; - this.state.renderMonthParams = this.createMonthProps(this.state); - } - - static defaultProps = { - initialDate: moment(), - scaleFactor: 375, - scrollable: false, - onDateChange: () => { - console.log('onDateChange() not provided'); - }, - enableDateChange: true, - headingLevel: 1, - sundayColor: '#FFFFFF', - customDatesStyles: [], - previousTitle: 'Previous', - nextTitle: 'Next', - selectMonthTitle: 'Select Month in ', - selectYearTitle: 'Select Year', - horizontal: true, - selectedDayStyle : null, - selectedRangeStartStyle: null, - selectedRangeEndStyle: null, - selectedRangeStyle: null, - }; - - componentDidUpdate(prevProps) { - let doStateUpdate = false; - - let newStyles = {}; - if ( - prevProps.width !== this.props.width || - prevProps.height !== this.props.height - ) { - newStyles = this.updateScaledStyles(this.props); - doStateUpdate = true; - } - - let newMonthYear = {}; - if (!moment(prevProps.initialDate).isSame(this.props.initialDate, 'day')) { - newMonthYear = this.updateMonthYear(this.props.initialDate); - doStateUpdate = true; - } - - let selectedDateRanges = {}; - const { selectedStartDate, selectedEndDate } = this.props; - if (selectedStartDate !== prevProps.selectedStartDate || - selectedEndDate !== prevProps.selectedEndDate - ) { - selectedDateRanges = { - selectedStartDate: selectedStartDate && moment(selectedStartDate), - selectedEndDate: selectedEndDate && moment(selectedEndDate) - }; - doStateUpdate = true; - } - - let disabledDates = {}; - if (prevProps.disabledDates !== this.props.disabledDates) { - disabledDates = this.updateDisabledDates(this.props.disabledDates); - doStateUpdate = true; - } - - let rangeDurations = {}; - if (prevProps.minRangeDuration !== this.props.minRangeDuration || - prevProps.maxRangeDuration !== this.props.maxRangeDuration - ) { - const {minRangeDuration, maxRangeDuration} = this.props; - rangeDurations = this.updateMinMaxRanges(minRangeDuration, maxRangeDuration); - doStateUpdate = true; - } - - let minMaxDates = {}; - if (prevProps.minDate !== this.props.minDate || - prevProps.minDate !== this.props.minDate - ) { - minMaxDates.minDate = this.props.minDate && moment(this.props.minDate); - minMaxDates.maxDate = this.props.maxDate && moment(this.props.maxDate); - doStateUpdate = true; - } - - if (prevProps.customDatesStyles !== this.props.customDatesStyles) { - // Update renderMonthParams on customDatesStyles change - doStateUpdate = true; - } - - if (doStateUpdate) { - const newState = { - ...newStyles, - ...newMonthYear, - ...selectedDateRanges, - ...disabledDates, - ...rangeDurations, - ...minMaxDates, - }; - let renderMonthParams = {}; - const _state = {...this.state, ...newState}; - renderMonthParams = this.createMonthProps(_state); - this.setState({...newState, renderMonthParams}); - } - } - - updateScaledStyles = props => { - const { - scaleFactor, - selectedDayColor, - selectedDayTextColor, - todayBackgroundColor, - width, - height, - dayShape - } = props; - - // The styles in makeStyles are intially scaled to this width - const containerWidth = width ? width : Dimensions.get('window').width; - const containerHeight = height ? height : Dimensions.get('window').height; - return { - styles: makeStyles({ - containerWidth, - containerHeight, - scaleFactor, - selectedDayColor, - selectedDayTextColor, - todayBackgroundColor, - dayShape - }) - }; - } - - updateMonthYear = (initialDate = this.props.initialDate, updateState) => { - const newState = { - currentMonth: parseInt(moment(initialDate).month()), - currentYear: parseInt(moment(initialDate).year()) - }; - if (updateState) { - this.setState(newState); - } - return newState; - } - - updateDisabledDates = (_disabledDates = []) => { - let disabledDates = []; - if (_disabledDates) { - if (Array.isArray(_disabledDates)) { - // Convert input date into timestamp - _disabledDates.map(date => { - let thisDate = moment(date); - thisDate.set({ hour: 12, minute: 0, second: 0, millisecond: 0 }); - disabledDates.push(thisDate.valueOf()); - }); - } - else if (_disabledDates instanceof Function) { - disabledDates = _disabledDates; - } - } - return { disabledDates }; - } - - updateMinMaxRanges = (_minRangeDuration, _maxRangeDuration) => { - let minRangeDuration = []; - let maxRangeDuration = []; - - if (_minRangeDuration) { - if (Array.isArray(_minRangeDuration)) { - _minRangeDuration.map(mrd => { - let thisDate = moment(mrd.date); - thisDate.set({ hour: 12, minute: 0, second: 0, millisecond: 0 }); - minRangeDuration.push({ - date: thisDate.valueOf(), - minDuration: mrd.minDuration - }); - }); - } else { - minRangeDuration = _minRangeDuration; - } - } - - if (_maxRangeDuration) { - if (Array.isArray(_maxRangeDuration)) { - _maxRangeDuration.map(mrd => { - let thisDate = moment(mrd.date); - thisDate.set({ hour: 12, minute: 0, second: 0, millisecond: 0 }); - maxRangeDuration.push({ - date: thisDate.valueOf(), - maxDuration: mrd.maxDuration - }); - }); - } else { - maxRangeDuration = _maxRangeDuration; - } - } - return {minRangeDuration, maxRangeDuration}; - } - - handleOnPressDay = ({year, month, day}) => { - const { - selectedStartDate: prevSelectedStartDate, - selectedEndDate: prevSelectedEndDate, - } = this.state; - - const { - allowRangeSelection, - allowBackwardRangeSelect, - enableDateChange, - onDateChange, - } = this.props; - - if (!enableDateChange) { - return; - } - - const date = moment({ year, month, day, hour: 12 }); - - if (allowRangeSelection && prevSelectedStartDate && !prevSelectedEndDate) { - if (date.isSameOrAfter(prevSelectedStartDate, 'day')) { - const selectedStartDate = prevSelectedStartDate; - const selectedEndDate = date; - this.setState({ - selectedEndDate, - renderMonthParams: this.createMonthProps({...this.state, selectedStartDate, selectedEndDate}), - }); - // Sync end date with parent - onDateChange(date, Utils.END_DATE); - } - else if (allowBackwardRangeSelect) { // date is before selectedStartDate - // Flip dates so that start is always before end. - const selectedEndDate = prevSelectedStartDate.clone(); - const selectedStartDate = date; - this.setState({ - selectedStartDate, - selectedEndDate, - renderMonthParams: this.createMonthProps({...this.state, selectedStartDate, selectedEndDate}), - }, () => { - // Sync both start and end dates with parent *after* state update. - onDateChange(this.state.selectedStartDate, Utils.START_DATE); - onDateChange(this.state.selectedEndDate, Utils.END_DATE); - }); - } - } else { - const syncEndDate = !!prevSelectedEndDate; - const selectedStartDate = date; - const selectedEndDate = null; - this.setState({ - selectedStartDate, - selectedEndDate, - renderMonthParams: this.createMonthProps({...this.state, selectedStartDate, selectedEndDate}), - }, () => { - // Sync start date with parent *after* state update. - onDateChange(this.state.selectedStartDate, Utils.START_DATE); - if (syncEndDate) { - // sync end date with parent - must be cleared if previously set. - onDateChange(null, Utils.END_DATE); - } - }); - } - } - - handleOnPressPrevious = () => { - const { currentMonth, currentYear } = this.state; - let previousMonth = currentMonth - 1; - let year = currentYear; - // if previousMonth is negative it means the current month is January, - // so we have to go back to previous year and set the current month to December - if (previousMonth < 0) { - previousMonth = 11; - year--; - } - const scrollFinisher = this.props.scrollable && this.scroller.scrollLeft; - this.handleOnPressFinisher({year, month: previousMonth, scrollFinisher}); - } - - handleOnPressNext = () => { - const { currentMonth, currentYear } = this.state; - let nextMonth = currentMonth + 1; - let year = currentYear; - // if nextMonth is greater than 11 it means the current month is December, - // so we have to go forward to the next year and set the current month to January - if (nextMonth > 11) { - nextMonth = 0; - year++; - } - const scrollFinisher = this.props.scrollable && this.scroller.scrollRight; - this.handleOnPressFinisher({year, month: nextMonth, scrollFinisher}); - } - - handleOnPressFinisher = ({year, month, scrollFinisher, extraState}) => { - if (scrollFinisher) { - scrollFinisher(); - } - else { - const currentMonth = parseInt(month); - const currentYear = parseInt(year); - const renderMonthParams = extraState || { - renderMonthParams: {...this.state.renderMonthParams, month, year} - }; - this.setState({ currentMonth, currentYear, ...renderMonthParams }); - } - const currentMonthYear = moment({year, month, hour: 12}); - this.props.onMonthChange && this.props.onMonthChange(currentMonthYear); - } - - handleOnPressYear = () => { - this.setState({ - currentView: 'years' - }); - } - - handleOnPressMonth = () => { - this.setState({ - currentView: 'months' - }); - } - - handleOnSelectMonthYear = ({month, year}) => { - const currentYear = year; - const currentMonth = month; - const scrollableState = this.props.scrollable ? { - ...this.createMonths(this.props, {currentYear, currentMonth}), - } : {}; - - const extraState = { - renderMonthParams: {...this.state.renderMonthParams, month, year}, - currentView: 'days', - ...scrollableState, - }; - - this.handleOnPressFinisher({month, year, extraState}); - } - - resetSelections = () => { - this.setState((state) => ({ - selectedStartDate: null, - selectedEndDate: null, - renderMonthParams: { - ...state.renderMonthParams, - selectedStartDate: null, - selectedEndDate: null, - } - })); - } - - createMonthProps = state => { - return { - onPressDay: this.handleOnPressDay, - month: state.currentMonth, - year: state.currentYear, - styles: state.styles, - disabledDates: state.disabledDates, - minDate: state.minDate, - maxDate: state.maxDate, - minRangeDuration: state.minRangeDuration, - maxRangeDuration: state.maxRangeDuration, - selectedStartDate: state.selectedStartDate, - selectedEndDate: state.selectedEndDate, - enableDateChange: this.props.enableDateChange, - firstDay: this.props.startFromMonday ? 1 : this.props.firstDay, - allowRangeSelection: this.props.allowRangeSelection, - allowBackwardRangeSelect: this.props.allowBackwardRangeSelect, - showDayStragglers: this.props.showDayStragglers, - disabledDatesTextStyle: this.props.disabledDatesTextStyle, - textStyle: this.props.textStyle, - todayTextStyle: this.props.todayTextStyle, - selectedDayTextStyle: this.props.selectedDayTextStyle, - selectedRangeStartTextStyle: this.props.selectedRangeStartTextStyle, - selectedRangeEndTextStyle: this.props.selectedRangeEndTextStyle, - selectedDayStyle: this.props.selectedDayStyle, - selectedDisabledDatesTextStyle: this.props.selectedDisabledDatesTextStyle, - selectedRangeStartStyle: this.props.selectedRangeStartStyle, - selectedRangeStyle: this.props.selectedRangeStyle, - selectedRangeEndStyle: this.props.selectedRangeEndStyle, - customDatesStyles: this.props.customDatesStyles, - }; - } - - createMonths = (props, {currentMonth, currentYear}) => { - if (!props.scrollable) { - return []; - } - - const { - initialDate, - minDate, - maxDate, - restrictMonthNavigation, - } = props; - - let monthsList = []; - let numMonths = this.numMonthsScroll; - let initialScrollerIndex = 0; - - // Center start month in scroller. Visible month is either the initialDate - // prop, or the current month & year that has been selected. - let _initialDate = Number.isInteger(currentMonth) && Number.isInteger(currentYear) && - moment({ year: currentYear, month: currentMonth, hour: 12 }); - _initialDate = _initialDate || initialDate; - let firstScrollerMonth = _initialDate.clone().subtract(numMonths/2, 'months'); - if (minDate && restrictMonthNavigation && firstScrollerMonth.isBefore(minDate, 'month')) { - firstScrollerMonth = moment(minDate); - } - - for (let i = 0; i < numMonths; i++) { - let month = firstScrollerMonth.clone().add(i, 'months'); - if (maxDate && restrictMonthNavigation && month.isAfter(maxDate, 'month')) { - break; - } - if (month.isSame(_initialDate, 'month')) { - initialScrollerIndex = i; - } - monthsList.push(month); - } - - return { - monthsList, - initialScrollerIndex, - }; - } - - renderMonth(props) { - return ( - - ); - } - - render() { - const { - currentView, - currentMonth, - currentYear, - minDate, - maxDate, - styles, - monthsList, - renderMonthParams, - initialScrollerIndex, - } = this.state; - - const { - startFromMonday, - firstDay, - initialDate, - weekdays, - months, - previousComponent, - nextComponent, - previousTitle, - nextTitle, - previousTitleStyle, - nextTitleStyle, - monthTitleStyle, - yearTitleStyle, - textStyle, - restrictMonthNavigation, - headingLevel, - dayLabelsWrapper, - customDayHeaderStyles, - selectMonthTitle, - selectYearTitle, - monthYearHeaderWrapperStyle, - headerWrapperStyle, - onMonthChange, - scrollable, - horizontal, - } = this.props; - - let content; - switch (currentView) { - case 'months': - content = ( - - ); - break; - case 'years': - content = ( - - ); - break; - default: - content = ( - - - - { scrollable ? - this.scroller = scroller} - data={monthsList} - renderMonth={this.renderMonth} - renderMonthParams={renderMonthParams} - maxSimultaneousMonths={this.numMonthsScroll} - initialRenderIndex={initialScrollerIndex} - minDate={minDate} - maxDate={maxDate} - restrictMonthNavigation={restrictMonthNavigation} - updateMonthYear={this.updateMonthYear} - onMonthChange={onMonthChange} - horizontal={horizontal} - /> - : - this.renderMonth(renderMonthParams) - } - - ); - } - - return content; - } -} +import React, { Component } from 'react'; +import { View, Dimensions } from 'react-native'; +import { makeStyles } from './makeStyles'; +import { Utils } from './Utils'; +import HeaderControls from './HeaderControls'; +import Weekdays from './Weekdays'; +import DaysGridView from './DaysGridView'; +import MonthSelector from './MonthSelector'; +import YearSelector from './YearSelector'; +import Swiper from './Swiper'; +import moment from 'moment'; + +const SWIPE_LEFT = 'SWIPE_LEFT'; +const SWIPE_RIGHT = 'SWIPE_RIGHT'; + +const _swipeConfig = { + velocityThreshold: 0.3, + directionalOffsetThreshold: 80 +}; + +export default class CalendarPicker extends Component { + constructor(props) { + super(props); + this.state = { + currentMonth: null, + currentYear: null, + currentView: 'days', + selectedStartDate: props.selectedStartDate && moment(props.selectedStartDate), + selectedEndDate: props.selectedEndDate && moment(props.selectedEndDate), + minDate: props.minDate && moment(props.minDate), + maxDate: props.maxDate && moment(props.maxDate), + styles: {}, + ...this.updateScaledStyles(props), + ...this.updateMonthYear(props.initialDate), + ...this.updateDayOfWeekStyles(props.initialDate), + ...this.updateDisabledDates(props.disabledDates), + ...this.updateMinMaxRanges(props.minRangeDuration, props.maxRangeDuration), + }; + this.updateScaledStyles = this.updateScaledStyles.bind(this); + this.updateMonthYear = this.updateMonthYear.bind(this); + this.updateDisabledDates = this.updateDisabledDates.bind(this); + this.updateMinMaxRanges = this.updateMinMaxRanges.bind(this); + this.updateDayOfWeekStyles = this.updateDayOfWeekStyles.bind(this); + this.handleOnPressPrevious = this.handleOnPressPrevious.bind(this); + this.handleOnPressNext = this.handleOnPressNext.bind(this); + this.handleOnPressDay = this.handleOnPressDay.bind(this); + this.handleOnPressMonth = this.handleOnPressMonth.bind(this); + this.handleOnPressYear = this.handleOnPressYear.bind(this); + this.handleOnSelectMonthYear = this.handleOnSelectMonthYear.bind(this); + this.onSwipe = this.onSwipe.bind(this); + this.resetSelections = this.resetSelections.bind(this); + } + + static defaultProps = { + lang: 'en', + initialDate: moment(), + scaleFactor: 375, + enableSwipe: true, + onDateChange: () => { + console.log('onDateChange() not provided'); + }, + enableDateChange: true, + headingLevel: 1, + sundayColor: '#FFFFFF', + customDatesStyles: [], + customDatesStylesPriority: 'dayOfWeek', // ToDo: Deprecated. Remove. + dayOfWeekStyles: {}, // ToDo: Deprecated. Remove. + previousTitle: 'Previous', + nextTitle: 'Next', + selectMonthTitle: 'Select Month', + selectYearTitle: 'Select Year', + }; + + componentDidMount() { + } + + componentDidUpdate(prevProps) { + let doStateUpdate = false; + + let newStyles = {}; + if ( + prevProps.width !== this.props.width || + prevProps.height !== this.props.height + ) { + newStyles = this.updateScaledStyles(this.props); + doStateUpdate = true; + } + + let newMonthYear = {}; + if (!moment(prevProps.initialDate).isSame(this.props.initialDate, 'day')) { + newMonthYear = this.updateMonthYear(this.props.initialDate); + doStateUpdate = true; + } + + let selectedDateRanges = {}; + const { selectedStartDate, selectedEndDate } = this.props; + if (selectedStartDate !== prevProps.selectedStartDate || + selectedEndDate !== prevProps.selectedEndDate + ) { + selectedDateRanges = { + selectedStartDate: selectedStartDate && moment(selectedStartDate), + selectedEndDate: selectedEndDate && moment(selectedEndDate) + }; + doStateUpdate = true; + } + + // ---------------------------------------------------------------- + // ToDo: Deprecated. Remove entire block + let customDatesStyles = {}; + if (this.props.startFromMonday !== prevProps.startFromMonday || + this.props.dayOfWeekStyles !== prevProps.dayOfWeekStyles || + this.props.customDatesStylesPriority !== prevProps.customDatesStylesPriority || + this.props.customDatesStyles !== prevProps.customDatesStyles + ) { + customDatesStyles = this.updateDayOfWeekStyles( + moment({ year: this.state.currentYear, month: this.state.currentMonth }), + ); + doStateUpdate = true; + } + // ---------------------------------------------------------------- + + let disabledDates = {}; + if (prevProps.disabledDates !== this.props.disabledDates) { + disabledDates = this.updateDisabledDates(this.props.disabledDates); + doStateUpdate = true; + } + + let rangeDurations = {}; + if (prevProps.minRangeDuration !== this.props.minRangeDuration || + prevProps.maxRangeDuration !== this.props.maxRangeDuration + ) { + const { minRangeDuration, maxRangeDuration } = this.props; + rangeDurations = this.updateMinMaxRanges(minRangeDuration, maxRangeDuration); + doStateUpdate = true; + } + + let minDate = this.props.minDate && moment(this.props.minDate); + let maxDate = this.props.maxDate && moment(this.props.maxDate); + + if (doStateUpdate) { + this.setState({ + ...newStyles, ...newMonthYear, ...selectedDateRanges, + ...customDatesStyles, ...disabledDates, ...rangeDurations, + minDate, maxDate + }); + } + } + + updateScaledStyles(props) { + const { + scaleFactor, + selectedDayColor, + selectedDayTextColor, + todayBackgroundColor, + width, + height, + dayShape + } = props; + + // The styles in makeStyles are intially scaled to this width + const containerWidth = width ? width : Dimensions.get('window').width; + const containerHeight = height ? height : Dimensions.get('window').height; + return { + styles: makeStyles({ + containerWidth, + containerHeight, + scaleFactor, + selectedDayColor, + selectedDayTextColor, + todayBackgroundColor, + dayShape + }) + }; + } + + updateMonthYear(initialDate = this.props.initialDate) { + return { + currentMonth: parseInt(moment(initialDate).month()), + currentYear: parseInt(moment(initialDate).year()) + }; + } + + updateDisabledDates(_disabledDates = []) { + let disabledDates = []; + if (_disabledDates) { + if (Array.isArray(_disabledDates)) { + // Convert input date into timestamp + _disabledDates.map(date => { + let thisDate = moment(date); + thisDate.set({ hour: 12, minute: 0, second: 0, millisecond: 0 }); + disabledDates.push(thisDate.valueOf()); + }); + } + else if (_disabledDates instanceof Function) { + disabledDates = _disabledDates; + } + } + return { disabledDates }; + } + + updateMinMaxRanges(_minRangeDuration, _maxRangeDuration) { + let minRangeDuration = []; + let maxRangeDuration = []; + + if (_minRangeDuration) { + if (Array.isArray(_minRangeDuration)) { + _minRangeDuration.map(mrd => { + let thisDate = moment(mrd.date); + thisDate.set({ hour: 12, minute: 0, second: 0, millisecond: 0 }); + minRangeDuration.push({ + date: thisDate.valueOf(), + minDuration: mrd.minDuration + }); + }); + } else { + minRangeDuration = _minRangeDuration; + } + } + + if (_maxRangeDuration) { + if (Array.isArray(_maxRangeDuration)) { + _maxRangeDuration.map(mrd => { + let thisDate = moment(mrd.date); + thisDate.set({ hour: 12, minute: 0, second: 0, millisecond: 0 }); + maxRangeDuration.push({ + date: thisDate.valueOf(), + maxDuration: mrd.maxDuration + }); + }); + } else { + maxRangeDuration = _maxRangeDuration; + } + } + return { minRangeDuration, maxRangeDuration }; + } + + handleOnPressDay(day) { + const { + currentYear, + currentMonth, + selectedStartDate, + selectedEndDate + } = this.state; + + const { + allowRangeSelection, + allowBackwardRangeSelect, + enableDateChange, + onDateChange, + } = this.props; + + if (!enableDateChange) { + return; + } + + const date = moment({ year: currentYear, month: currentMonth, day, hour: 12 }); + + if (allowRangeSelection && selectedStartDate && !selectedEndDate) { + if (date.isSameOrAfter(selectedStartDate, 'day')) { + this.setState({ + selectedEndDate: date + }); + // Sync start date with parent + onDateChange(date, Utils.END_DATE); + } + else if (allowBackwardRangeSelect) { // date is before selectedStartDate + // Flip dates so that start is always before end. + const endDate = selectedStartDate.clone(); + this.setState({ + selectedStartDate: date, + selectedEndDate: endDate + }, () => { + // Sync both start and end dates with parent *after* state update. + onDateChange(this.state.selectedStartDate, Utils.START_DATE); + onDateChange(this.state.selectedEndDate, Utils.END_DATE); + }); + } + } else { + const syncEndDate = !!selectedEndDate; + this.setState({ + selectedStartDate: date, + selectedEndDate: null + }, () => { + // Sync start date with parent *after* state update. + onDateChange(this.state.selectedStartDate, Utils.START_DATE); + if (syncEndDate) { + // sync end date with parent - must be cleared if previously set. + onDateChange(null, Utils.END_DATE); + } + }); + } + } + + // ---------------------------------------------------------------- + // ToDo: Deprecated. Remove entire function and refactor accordingly. + updateDayOfWeekStyles(currentDate) { + if (this.props.customDatesStyles instanceof Function) { + return { customDatesStyles: this.props.customDatesStyles }; + } + + const { + startFromMonday, + dayOfWeekStyles, //ToDo: Deprecated. Remove. + customDatesStyles: propsCustomDatesStyles, + customDatesStylesPriority + } = this.props; + + let day = moment(currentDate).startOf('month'); + let customDayOfWeekStyles = []; + do { + let dayIndex = day.day(); + if (startFromMonday) { + dayIndex = dayIndex - 1; + if (dayIndex < 0) { + dayIndex = 6; // This is Sunday. + } + } + let currentDayStyle = dayOfWeekStyles[dayIndex]; + if (currentDayStyle) { + console.warn('CalendarPicker: dayOfWeekStyles is deprecated. Use customDatesStyles / customDayHeaderStyles callbacks instead.'); + customDayOfWeekStyles.push({ + date: day.clone(), + textStyle: currentDayStyle, + }); + } + } while (day.add(1, 'day').isSame(currentDate, 'month')); + + let customDatesStyles = []; + if (customDatesStylesPriority === 'dayOfWeek') { + customDatesStyles = [...customDayOfWeekStyles, ...propsCustomDatesStyles]; + } + else { + console.warn('CalendarPicker: customDatesStylesPriority is deprecated. Use customDatesStyles / customDayHeaderStyles callbacks instead.'); + customDatesStyles = [...propsCustomDatesStyles, ...customDayOfWeekStyles]; + } + + return { customDatesStyles }; + } + // ---------------------------------------------------------------- + + handleOnPressPrevious() { + let { currentMonth, currentYear } = this.state; + let previousMonth = currentMonth - 1; + // if previousMonth is negative it means the current month is January, + // so we have to go back to previous year and set the current month to December + if (previousMonth < 0) { + previousMonth = 11; + currentYear--; + } + this.handleOnPressFinisher({ year: currentYear, month: previousMonth }); + } + + handleOnPressNext() { + let { currentMonth, currentYear } = this.state; + let nextMonth = currentMonth + 1; + // if nextMonth is greater than 11 it means the current month is December, + // so we have to go forward to the next year and set the current month to January + if (nextMonth > 11) { + nextMonth = 0; + currentYear++; + } + this.handleOnPressFinisher({ year: currentYear, month: nextMonth }); + } + + handleOnPressFinisher({ year, month }) { + // ---------------------------------------------------------------- + // ToDo: Deprecated. Remove + let dayOfWeekStyles = {}; + let currentMonthYear = moment({ year, month }); + try { + if (Object.entries(this.props.dayOfWeekStyles).length) { + dayOfWeekStyles = this.updateDayOfWeekStyles(currentMonthYear); + } + } + catch (error) { + console.log('dayOfWeekStyles error'); + } + // ---------------------------------------------------------------- + + this.setState({ + ...dayOfWeekStyles, //ToDo: Deprecated. Remove. + currentMonth: parseInt(month), + currentYear: parseInt(year) + }); + + this.props.onMonthChange && this.props.onMonthChange(currentMonthYear); + } + + handleOnPressYear() { + this.setState({ + currentView: 'years' + }); + } + + handleOnPressMonth() { + this.setState({ + currentView: 'months' + }); + } + + handleOnSelectMonthYear({ month, year }) { + this.setState({ + currentYear: year, + currentMonth: month, + currentView: 'days' + }); + } + + onSwipe(gestureName) { + if (typeof this.props.onSwipe === 'function') { + this.props.onSwipe(gestureName); + return; + } + switch (gestureName) { + case SWIPE_LEFT: + this.handleOnPressNext(); + break; + case SWIPE_RIGHT: + this.handleOnPressPrevious(); + break; + } + } + + resetSelections() { + this.setState({ + selectedStartDate: null, + selectedEndDate: null + }); + } + + render() { + const { + currentMonth, + currentYear, + minDate, + maxDate, + minRangeDuration, + maxRangeDuration, + selectedStartDate, + selectedEndDate, + disabledDates, + styles, + customDatesStyles, + } = this.state; + + const { + lang, + allowRangeSelection, + allowBackwardRangeSelect, + startFromMonday, + initialDate, + weekdays, + months, + previousComponent, + nextComponent, + previousTitle, + nextTitle, + previousTitleStyle, + nextTitleStyle, + textStyle, + todayTextStyle, + selectedDayStyle, + selectedRangeStartStyle, + selectedRangeStyle, + selectedRangeEndStyle, + disabledDatesTextStyle, + swipeConfig, + enableDateChange, + restrictMonthNavigation, + headingLevel, + dayLabelsWrapper, + dayOfWeekStyles, // ToDo: Deprecated. Remove. + customDayHeaderStyles, + selectMonthTitle, + selectYearTitle, + showDayStragglers, + monthYearHeaderWrapperStyle, + } = this.props; + + let content; + switch (this.state.currentView) { + case 'months': + content = ( + + ); + break; + case 'years': + content = ( + + ); + break; + default: + content = ( + + + + + + ); + } + + return ( + this.props.enableSwipe && this.onSwipe(direction)} + config={{ ..._swipeConfig, ...swipeConfig }} + > + + {content} + + + ); + } +} diff --git a/CalendarPicker/localPropTypes.js b/CalendarPicker/localPropTypes.js deleted file mode 100644 index b4ad5681..00000000 --- a/CalendarPicker/localPropTypes.js +++ /dev/null @@ -1,7 +0,0 @@ -// Prop type definitions required for ambiguities and deficiencies in the -// prop-types package. -// These may need to be tweaked as prop types are improved. - -import PropTypes from 'prop-types'; - -export const stylePropType = PropTypes.any; diff --git a/CalendarPicker/makeStyles.js b/CalendarPicker/makeStyles.js index 50b6bce5..3bfe7eca 100644 --- a/CalendarPicker/makeStyles.js +++ b/CalendarPicker/makeStyles.js @@ -1,276 +1,266 @@ -/** - * Calendar Picker Component - * - * Copyright 2016 Yahoo Inc. - * Licensed under the terms of the MIT license. See LICENSE file in the project root for terms. - */ -const DEFAULT_SELECTED_BACKGROUND_COLOR = '#5ce600'; -const DEFAULT_SELECTED_TEXT_COLOR = '#000000'; -const DEFAULT_TODAY_BACKGROUND_COLOR = '#CCCCCC'; - -function getBorderRadiusByShape(scaler, dayShape) { - if (dayShape === 'square') { - return 0; - } else { - return 30*scaler; - } -} - -export function makeStyles(params) { - const { - containerWidth, - containerHeight, - scaleFactor, - selectedDayColor, - selectedDayTextColor, - todayBackgroundColor, - dayShape - } = params; - const scaler = Math.min(containerWidth, containerHeight) / scaleFactor; - const SELECTED_BG_COLOR = selectedDayColor ? selectedDayColor : DEFAULT_SELECTED_BACKGROUND_COLOR; - const SELECTED_TEXT_COLOR = selectedDayTextColor ? selectedDayTextColor : DEFAULT_SELECTED_TEXT_COLOR; - const TODAY_BG_COLOR = todayBackgroundColor ? todayBackgroundColor : DEFAULT_TODAY_BACKGROUND_COLOR; - - return { - containerWidth, - containerHeight, - - calendar: { - height: 320*scaler, - marginTop: 10*scaler - }, - - dayButton: { - width: 30*scaler, - height: 30*scaler, - borderRadius: getBorderRadiusByShape(scaler, dayShape), - alignSelf: 'center', - justifyContent: 'center' - }, - - dayLabel: { - fontSize: 14*scaler, - color: '#000', - alignSelf: 'center' - }, - - selectedDayLabel: { - color: SELECTED_TEXT_COLOR, - }, - - dayLabelsWrapper: { - flexDirection: 'row', - borderBottomWidth: 1, - borderTopWidth: 1, - paddingTop: 10*scaler, - paddingBottom: 10*scaler, - alignSelf: 'center', - justifyContent: 'center', - backgroundColor: 'rgba(0,0,0,0.0)', - borderColor: 'rgba(0,0,0,0.2)' - }, - - daysWrapper: { - alignSelf: 'center', - justifyContent: 'center' - }, - - dayLabels: { - width: 50*scaler, - fontSize: 12*scaler, - color: '#000', - textAlign: 'center' - }, - - selectedDay: { - width: 30*scaler, - height:30*scaler, - borderRadius: getBorderRadiusByShape(scaler, dayShape), - alignSelf: 'center', - justifyContent: 'center' - }, - - selectedDayBackground: { - backgroundColor: SELECTED_BG_COLOR, - }, - - selectedToday: { - width: 30*scaler, - height:30*scaler, - backgroundColor: TODAY_BG_COLOR, - borderRadius: getBorderRadiusByShape(scaler, dayShape), - alignSelf: 'center', - justifyContent: 'center' - }, - - dayWrapper: { - alignItems: 'center', - justifyContent: 'center', - width: 50*scaler, - height: 40*scaler, - backgroundColor: 'rgba(0,0,0,0.0)' - }, - - startDayWrapper: { - width: 50*scaler, - height: 30*scaler, - borderTopLeftRadius: 20*scaler, - borderBottomLeftRadius: 20*scaler, - backgroundColor: SELECTED_BG_COLOR, - alignSelf: 'center', - justifyContent: 'center' - }, - - endDayWrapper: { - width: 50*scaler, - height: 30*scaler, - borderTopRightRadius: 20*scaler, - borderBottomRightRadius: 20*scaler, - backgroundColor: SELECTED_BG_COLOR, - alignSelf: 'center', - justifyContent: 'center' - }, - - inRangeDay: { - width: 50*scaler, - height: 30*scaler, - backgroundColor: SELECTED_BG_COLOR, - alignSelf: 'center', - justifyContent: 'center' - }, - - headerWrapper: { - flexDirection: 'row', - alignItems: 'center', - alignSelf: 'center', - justifyContent: 'space-between', - width: containerWidth, - padding: 5*scaler, - paddingBottom: 3*scaler, - marginBottom: 10*scaler, - backgroundColor: 'rgba(0,0,0,0.0)' - }, - - monthYearHeaderWrapper: { - flexDirection: 'row', - justifyContent: 'center', - alignItems: 'center', - paddingHorizontal: 3*scaler, - }, - - previousContainer: { - marginLeft: 10*scaler, - }, - - nextContainer: { - marginRight: 10*scaler, - alignItems: 'flex-end', - }, - - navButtonText: { - fontSize: 14*scaler, - }, - - weeks: { - flexDirection: 'column' - }, - - weekRow: { - flexDirection: 'row' - }, - - disabledText: { - fontSize: 14*scaler, - color: '#BBBBBB', - alignSelf: 'center', - justifyContent: 'center' - }, - - selectedDisabledText: { - fontSize: 14*scaler, - color: '#DDDDDD', - alignSelf: 'center', - justifyContent: 'center' - }, - - monthHeaderMainText: { - fontSize: 16*scaler, - color: '#000', - textAlign: 'right', - marginHorizontal: 3*scaler, - }, - - monthButton: { - width: 30*scaler, - height: 30*scaler, - borderRadius: 30*scaler, - alignSelf: 'center', - justifyContent: 'center' - }, - - monthsHeaderText: { - flex: 1, - fontSize: 16*scaler, - color: '#000', - textAlign: 'center' - }, - - monthContainer: { - flex: 1, - alignItems: 'center', - }, - - monthText: { - fontSize: 14*scaler, - color: '#000', - alignSelf: 'center' - }, - - monthsWrapper: { - alignSelf: 'center', - justifyContent: 'center', - width: containerWidth, - }, - - monthsRow: { - flexDirection: 'row', - padding: 20*scaler, - }, - - yearHeaderMainText: { - fontSize: 16*scaler, - color: '#000', - marginHorizontal: 3*scaler, - }, - - yearContainer: { - flex: 1, - alignItems: 'center', - }, - - yearText: { - fontSize: 14*scaler, - color: '#000', - alignSelf: 'center' - }, - - yearsHeaderText: { - fontSize: 16*scaler, - color: '#000', - width: 180*scaler, - textAlign: 'center' - }, - - yearsWrapper: { - alignSelf: 'center', - justifyContent: 'center', - width: containerWidth, - }, - - yearsRow: { - flexDirection: 'row', - padding: 20*scaler, - }, - - }; -} +/** + * Calendar Picker Component + * + * Copyright 2016 Yahoo Inc. + * Licensed under the terms of the MIT license. See LICENSE file in the project root for terms. + */ +const DEFAULT_SELECTED_BACKGROUND_COLOR = '#5ce600'; +const DEFAULT_SELECTED_TEXT_COLOR = '#000000'; +const DEFAULT_TODAY_BACKGROUND_COLOR = '#CCCCCC'; + +function getBorderRadiusByShape(scaler, dayShape) { + if (dayShape === 'square') { + return 0; + } else { + return 30*scaler; + } +} + +export function makeStyles(params) { + const { + containerWidth, + containerHeight, + scaleFactor, + selectedDayColor, + selectedDayTextColor, + todayBackgroundColor, + dayShape + } = params; + const scaler = Math.min(containerWidth, containerHeight) / scaleFactor; + const SELECTED_BG_COLOR = selectedDayColor ? selectedDayColor : DEFAULT_SELECTED_BACKGROUND_COLOR; + const SELECTED_TEXT_COLOR = selectedDayTextColor ? selectedDayTextColor : DEFAULT_SELECTED_TEXT_COLOR; + const TODAY_BG_COLOR = todayBackgroundColor ? todayBackgroundColor : DEFAULT_TODAY_BACKGROUND_COLOR; + + return { + calendar: { + height: 320*scaler, + marginTop: 10*scaler + }, + + dayButton: { + width: 30*scaler, + height: 30*scaler, + borderRadius: getBorderRadiusByShape(scaler, dayShape), + alignSelf: 'center', + justifyContent: 'center' + }, + + dayLabel: { + fontSize: 14*scaler, + color: '#000', + alignSelf: 'center' + }, + + selectedDayLabel: { + color: SELECTED_TEXT_COLOR, + }, + + dayLabelsWrapper: { + flexDirection: 'row', + borderBottomWidth: 1, + borderTopWidth: 1, + paddingTop: 10*scaler, + paddingBottom: 10*scaler, + alignSelf: 'center', + justifyContent: 'center', + backgroundColor: 'rgba(0,0,0,0.0)', + borderColor: 'rgba(0,0,0,0.2)' + }, + + daysWrapper: { + alignSelf: 'center', + justifyContent: 'center' + }, + + dayLabels: { + width: 50*scaler, + fontSize: 12*scaler, + color: '#000', + textAlign: 'center' + }, + + selectedDay: { + width: 30*scaler, + height:30*scaler, + borderRadius: getBorderRadiusByShape(scaler, dayShape), + alignSelf: 'center', + justifyContent: 'center' + }, + + selectedDayBackground: { + backgroundColor: SELECTED_BG_COLOR, + }, + + selectedToday: { + width: 30*scaler, + height:30*scaler, + backgroundColor: TODAY_BG_COLOR, + borderRadius: getBorderRadiusByShape(scaler, dayShape), + alignSelf: 'center', + justifyContent: 'center' + }, + + dayWrapper: { + alignItems: 'center', + justifyContent: 'center', + width: 50*scaler, + height: 40*scaler, + backgroundColor: 'rgba(0,0,0,0.0)' + }, + + startDayWrapper: { + width: 50*scaler, + height: 30*scaler, + borderTopLeftRadius: 20*scaler, + borderBottomLeftRadius: 20*scaler, + backgroundColor: SELECTED_BG_COLOR, + alignSelf: 'center', + justifyContent: 'center' + }, + + endDayWrapper: { + width: 50*scaler, + height: 30*scaler, + borderTopRightRadius: 20*scaler, + borderBottomRightRadius: 20*scaler, + backgroundColor: SELECTED_BG_COLOR, + alignSelf: 'center', + justifyContent: 'center' + }, + + inRangeDay: { + width: 50*scaler, + height: 30*scaler, + backgroundColor: SELECTED_BG_COLOR, + alignSelf: 'center', + justifyContent: 'center' + }, + + headerWrapper: { + flexDirection: 'row', + alignItems: 'center', + alignSelf: 'center', + justifyContent: 'space-between', + width: containerWidth, + padding: 5*scaler, + paddingBottom: 3*scaler, + marginBottom: 10*scaler, + backgroundColor: 'rgba(0,0,0,0.0)' + }, + + monthYearHeaderWrapper: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: 3*scaler, + }, + + previousContainer: { + marginLeft: 10*scaler, + }, + + nextContainer: { + marginRight: 10*scaler, + alignItems: 'flex-end', + }, + + navButtonText: { + fontSize: 14*scaler, + }, + + weeks: { + flexDirection: 'column' + }, + + weekRow: { + flexDirection: 'row' + }, + + disabledText: { + fontSize: 14*scaler, + color: '#BBBBBB', + alignSelf: 'center', + justifyContent: 'center' + }, + + monthHeaderMainText: { + fontSize: 16*scaler, + color: '#000', + textAlign: 'right', + marginHorizontal: 3*scaler, + }, + + monthButton: { + width: 30*scaler, + height: 30*scaler, + borderRadius: 30*scaler, + alignSelf: 'center', + justifyContent: 'center' + }, + + monthsHeaderText: { + flex: 1, + fontSize: 16*scaler, + color: '#000', + textAlign: 'center' + }, + + monthContainer: { + flex: 1, + alignItems: 'center', + }, + + monthText: { + fontSize: 14*scaler, + color: '#000', + alignSelf: 'center' + }, + + monthsWrapper: { + alignSelf: 'center', + justifyContent: 'center', + width: containerWidth, + }, + + monthsRow: { + flexDirection: 'row', + padding: 20*scaler, + }, + + yearHeaderMainText: { + fontSize: 16*scaler, + color: '#000', + marginHorizontal: 3*scaler, + }, + + yearContainer: { + flex: 1, + alignItems: 'center', + }, + + yearText: { + fontSize: 14*scaler, + color: '#000', + alignSelf: 'center' + }, + + yearsHeaderText: { + fontSize: 16*scaler, + color: '#000', + width: 180*scaler, + textAlign: 'center' + }, + + yearsWrapper: { + alignSelf: 'center', + justifyContent: 'center', + width: containerWidth, + }, + + yearsRow: { + flexDirection: 'row', + padding: 20*scaler, + }, + + }; +}