Skip to content
This repository has been archived by the owner on Aug 30, 2019. It is now read-only.

Commit

Permalink
Merge ca19875 into 895767e
Browse files Browse the repository at this point in the history
  • Loading branch information
mAiNiNfEcTiOn committed Mar 20, 2017
2 parents 895767e + ca19875 commit 0a2e9f9
Show file tree
Hide file tree
Showing 19 changed files with 13,371 additions and 30 deletions.
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"comma-dangle": ["error", "always-multiline"],
"import/no-extraneous-dependencies": ["error", {"devDependencies": true}],
"max-len": [2, {"code": 120, "comments": 120, "tabWidth": 4}],
"react/jsx-indent": [2, 2, { "indentLogicalExpressions": true }],
"react/react-in-jsx-scope": 0,
"no-else-return": 0
}
Expand Down
3 changes: 2 additions & 1 deletion builder/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const builder = require('./builder');
const pkg = require('../package.json');
const program = require('commander');
const util = require('util');

program
.version(pkg.version)
Expand All @@ -16,4 +17,4 @@ program.parse(process.argv);

builder(program)
.then(() => console.log('Done!'))
.catch(console.error);
.catch(e => console.error(util.inspect(e, true, undefined, true)));
36 changes: 36 additions & 0 deletions components/_helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,44 @@ function getDataAttributes(attributes = {}) {
}, {});
}

/**
* @function leftPad
* @param {Number} value The value to be left padded
* @return {String} The value with a leading 0 if it's between 1 - 9
*/
function leftPad(value, maxLength = 2, leftPaddedBy = '0') {
const valueStringified = value.toString();
if (valueStringified.length >= maxLength) {
return valueStringified;
}

return leftPaddedBy.repeat(maxLength - valueStringified.length) + valueStringified;
}

/**
* Receives a date object and normalizes it to the proper hours, minutes,
* seconds and milliseconds.
*
* @method normalizeDate
* @param {Date} dateObject Date object to be normalized.
* @param {Number} hours Value to set the hours to. Defaults to 0
* @param {Number} minutes Value to set the minutes to. Defaults to 0
* @param {Number} seconds Value to set the seconds to. Defaults to 0
* @param {Number} milliseconds Value to set the milliseconds to. Defaults to 0
* @return {Date} The normalized date object.
*/
function normalizeDate(dateObject, hours = 0, minutes = 0, seconds = 0, milliseconds = 0) {
dateObject.setHours(hours);
dateObject.setMinutes(minutes);
dateObject.setSeconds(seconds);
dateObject.setMilliseconds(milliseconds);
return dateObject;
}

// Exports
export default {
getClassNamesWithMods,
getDataAttributes,
leftPad,
normalizeDate,
};
306 changes: 306 additions & 0 deletions components/calendar/calendar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
import React, { Component, PropTypes } from 'react';
import { getClassNamesWithMods, getDataAttributes, normalizeDate } from '../_helpers';
import DaysPanel from './panels/days';
import calendarConstants from './constants/calendar';

const {
CALENDAR_MOVE_TO_NEXT,
CALENDAR_MOVE_TO_PREVIOUS,
CALENDAR_SELECTION_TYPE_RANGE,
} = calendarConstants;


/**
* Processes the given props and the existing state and returns
* a new state.
*
* @function processProps
* @param {Object} props Props to base the new state on.
* @param {Object} state (Existing) state to be based on for the existing values.
* @return {Object} New state to be set/used.
* @static
*/
function processProps(props) {
const { initialDates, maxDate, minDate, selectionType } = props;
const maxLimit = maxDate ? normalizeDate(new Date(maxDate), 23, 59, 59, 999) : null;
const renderDate = normalizeDate(((initialDates && initialDates.length && initialDates[0])
? new Date(initialDates[0])
: new Date()));

let minLimit = minDate ? normalizeDate(new Date(minDate)) : null;
let selectedDates = [null, null];

if (initialDates) {
selectedDates = selectedDates.map((item, idx) => {
if (!initialDates[idx]) {
return null;
}

return normalizeDate(new Date(initialDates[idx]));
});
}

/**
* If a minDate or a maxDate is set, let's check if any selectedDates are outside of the boundaries.
* If so, resets the selectedDates.
*/
if (minLimit || maxLimit) {
const isAnyDateOutOfLimit = selectedDates.some(item => (
item && (
(minLimit && (minLimit.getTime() > item.getTime())) ||
(maxLimit && (maxLimit.getTime() < item.getTime()))
)
));

if (isAnyDateOutOfLimit) {
selectedDates = [null, null];
console.warn(`A calendar instance contains a selectedDate outside of the minDate and maxDate boundaries`); // eslint-disable-line
}
}

/** If initialDates is defined and we have a start date, we want to set it as the minLimit */
if (selectedDates[0] && (selectionType === CALENDAR_SELECTION_TYPE_RANGE)) {
minLimit = selectedDates[0];
}

return {
maxLimit,
minLimit,
renderDate,
selectedDates,
};
}

export default class Calendar extends Component {
constructor(props) {
super();

this.moveToMonth = this.moveToMonth.bind(this);
this.state = processProps(props);
}

componentWillReceiveProps(newProps) {
const { initialDates, maxDate, minDate, selectionType } = newProps;

let propsChanged = (
(maxDate !== this.props.maxDate) ||
(minDate !== this.props.minDate) ||
(selectionType !== this.props.selectionType)
);

if (initialDates) {
if (this.props.initialDates) {
propsChanged = propsChanged || initialDates.some((item, idx) => item !== this.props.initialDates[idx]);
} else {
propsChanged = true;
}
}

if (propsChanged) {
this.setState(() => processProps(newProps));
}
}

/**
* Changes the renderDate of the calendar to the previous or next month.
* Also triggers the onNavPreviousMonth/onNavNextMonth when the state gets changed
* and passes the new date to it.
*
* @method moveToMonth
* @param {String} direction Defines to which month is the calendar moving (previous or next).
*/
moveToMonth(direction) {
const { onNavNextMonth, onNavPreviousMonth } = this.props;

this.setState(({ renderDate }) => {
renderDate.setMonth(renderDate.getMonth() + (direction === CALENDAR_MOVE_TO_PREVIOUS ? -1 : 1));
return { renderDate };
}, () => {
if ((direction === CALENDAR_MOVE_TO_PREVIOUS) && onNavPreviousMonth) {
onNavPreviousMonth(this.state.renderDate);
} else if ((direction === CALENDAR_MOVE_TO_NEXT) && onNavNextMonth) {
onNavNextMonth(this.state.renderDate);
}
});
}

/**
* Handler for the day's selection. Passed to the DaysPanel -> DaysView.
* Also triggers the onSelectDay function (when passed) after the state is updated,
* passing the selectedDates array to it.
*
* @method onSelectDay
* @param {Date} dateSelected Date selected by the user.
*/
onSelectDay(dateSelected) {
const { onSelectDay, selectionType, minDate } = this.props;

this.setState((prevState) => {
let { minLimit, renderDate, selectedDates } = prevState;

/**
* If the calendar's selectionType is 'normal', we always set the date selected
* to the first position of the selectedDates array.
* If the selectionType is 'range', we need to verify the following requirements:
*
* - If there's no start date selected, then the selected date becomes the start
* date and the minLimit becomes that same date. Prevents the range selection to the past.
*
* - If there's a start date already selected:
*
* - If there's no end date selected, then the selected date becomes the end date. Also
* if the start and end dates are the same, it will remove the minLimit as the layout renders
* them as a 'normal' selection.
*
* - If there's an end date selected and the user is clicking on the start date again, it
* clears the selections and the limits, resetting the range.
*/
if (selectionType === CALENDAR_SELECTION_TYPE_RANGE) {
if (selectedDates[0]) {
if (!selectedDates[1]) {
selectedDates[1] = dateSelected;
if (selectedDates[0].toDateString() === selectedDates[1].toDateString()) {
minLimit = minDate ? normalizeDate(new Date(minDate)) : null;
}
} else {
selectedDates = [null, null];
minLimit = minDate ? normalizeDate(new Date(minDate)) : null;
}
} else {
selectedDates[0] = dateSelected;
minLimit = dateSelected;
selectedDates[1] = null;
}
} else {
selectedDates[0] = dateSelected;
}

/**
* If the user selects a day of the previous or next month, the rendered month switches to
* the one of the selected date.
*/
if (dateSelected.getMonth() !== renderDate.getMonth()) {
renderDate = new Date(dateSelected.toDateString());
}

return {
minLimit,
renderDate,
selectedDates,
};
}, () => {
if (onSelectDay) {
onSelectDay(this.state.selectedDates);
}
});
}

render() {
const { dataAttrs = {}, isDaySelectableFn, locale, mods = [], navButtons, selectionType } = this.props;
const { maxLimit, minLimit, renderDate, selectedDates } = this.state;

const restProps = getDataAttributes(dataAttrs);
const className = getClassNamesWithMods('ui-calendar', mods);

return (
<div className={className} {...restProps}>
<DaysPanel
isDaySelectableFn={isDaySelectableFn}
locale={locale}
maxDate={maxLimit}
minDate={minLimit}
navButtons={navButtons}
onNavNextMonth={() => this.moveToMonth(CALENDAR_MOVE_TO_NEXT)}
onNavPreviousMonth={() => this.moveToMonth(CALENDAR_MOVE_TO_PREVIOUS)}
onSelectDay={dt => this.onSelectDay(dt)}
renderDate={renderDate}
selectedDates={selectedDates}
selectionType={selectionType}
/>
</div>
);
}
}

Calendar.defaultProps = {
selectionType: 'normal',
};

Calendar.propTypes = {
/**
* Data attribute. You can use it to set up GTM key or any custom data-* attribute
*/
dataAttrs: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.object,
]),

/**
* Optional. Initial value of the calendar. Defaults to the current date as per the locale.
*/
initialDates: PropTypes.array,

/**
* Optional. Function to be triggered to evaluate if the date (passed as an argument)
* is selectable. Must return a boolean.
*/
isDaySelectableFn: PropTypes.func,

/**
* Locale definitions, with the calendar's months and weekdays in the right language.
* Also contains the startWeekDay which defines in which week day starts the week.
*/
locale: PropTypes.shape({
months: PropTypes.array,
weekDays: PropTypes.array,
startWeekDay: PropTypes.number,
}),

/**
* Sets the max date boundary. Defaults to `null`.
*/
maxDate: PropTypes.string,

/**
* Sets the min date boundary. Defaults to `null`.
*/
minDate: PropTypes.string,

/**
* You can provide set of custom modifications.
*/
mods: PropTypes.arrayOf(PropTypes.string),

navButtons: PropTypes.shape({
days: PropTypes.shape({
next: PropTypes.shape({
ariaLabel: PropTypes.string,
displayValue: PropTypes.string,
}),
previous: PropTypes.shape({
ariaLabel: PropTypes.string,
displayValue: PropTypes.string,
}),
}),
}),

/**
* Function to be triggered when pressing the nav's "next" button.
*/
onNavNextMonth: PropTypes.func,

/**
* Function to be triggered when pressing the nav's "previous" button.
*/
onNavPreviousMonth: PropTypes.func,

/**
* Function to be triggered when selecting a day.
*/
onSelectDay: PropTypes.func,

/**
* Optional. Type of date selection.
*/
selectionType: PropTypes.oneOf(['normal', 'range']),
};
10 changes: 10 additions & 0 deletions components/calendar/calendar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Basic calendar:

<div>
<Calendar
initialDates={["2017-03-21","2017-03-29"]}
onSelectDay={dts => document.getElementById('calendar-output').value = dts}
selectionType="range"
/><br />
<output id="calendar-output">Please select a date!</output><br />
</div>

0 comments on commit 0a2e9f9

Please sign in to comment.