diff --git a/README.md b/README.md index fb69aac..203410e 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ import AvailableTimes from 'react-available-times'; ]} height={400} recurring={false} + availableDays={['monday', 'tuesday', 'wednesday', 'thursday', 'friday']} + availableHourRange={{ start: 9, end: 19 }} /> ``` @@ -74,6 +76,10 @@ None of the props are required. with events that have a start and end expressed in number of minutes since the start of the week. The `weekStartsOn` prop is taken into account here, so the `0` minute is either monday at 00:00 or sunday at 00:00. +- `availableDays`: an array of strings (`"monday"`, `"tuesday"` ...) specifying + what days of the week are available to be used. It is set to every day by default. +- `availableHourRange`: an object with `start` and `end` numbers, ranging from 0 to 24 + inclusive. Defaults to the entire day by default. ## Contributing diff --git a/package.json b/package.json index e2bc693..282d056 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-available-times", - "version": "1.1.2", + "version": "1.2.0", "description": "A calendar to pick available time slots", "main": "dist/main.js", "repository": "git@github.com:trotzig/react-available-times.git", diff --git a/src/AvailableTimes.jsx b/src/AvailableTimes.jsx index b1b32c2..4b4ed12 100644 --- a/src/AvailableTimes.jsx +++ b/src/AvailableTimes.jsx @@ -2,7 +2,8 @@ import PropTypes from 'prop-types'; import React, { PureComponent } from 'react'; import momentTimezone from 'moment-timezone'; -import { WEEKS_PER_TIMESPAN } from './Constants'; +import { WEEKS_PER_TIMESPAN, DAYS_IN_WEEK } from './Constants'; +import { validateDays } from './Validators'; import CalendarSelector from './CalendarSelector'; import EventsStore from './EventsStore'; import Slider from './Slider'; @@ -216,6 +217,8 @@ export default class AvailableTimes extends PureComponent { timeZone, recurring, touchToDeleteSelection, + availableDays, + availableHourRange, } = this.props; const { @@ -298,6 +301,8 @@ export default class AvailableTimes extends PureComponent { height={height} recurring={recurring} touchToDeleteSelection={touchToDeleteSelection} + availableDays={availableDays} + availableHourRange={availableHourRange} /> ); })} @@ -340,10 +345,17 @@ AvailableTimes.propTypes = { height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), recurring: PropTypes.bool, touchToDeleteSelection: PropTypes.bool, + availableDays: PropTypes.arrayOf(validateDays), + availableHourRange: PropTypes.shape({ + start: PropTypes.number, + end: PropTypes.number, + }).isRequired, }; AvailableTimes.defaultProps = { timeZone: momentTimezone.tz.guess(), weekStartsOn: 'sunday', touchToDeleteSelection: 'ontouchstart' in window, + availableDays: DAYS_IN_WEEK, + availableHourRange: { start: 0, end: 24 }, }; diff --git a/src/Constants.js b/src/Constants.js index 2c41d75..c8a7aed 100644 --- a/src/Constants.js +++ b/src/Constants.js @@ -2,3 +2,4 @@ export const HOUR_IN_PIXELS = 50; export const MINUTE_IN_PIXELS = HOUR_IN_PIXELS / 60; export const RULER_WIDTH_IN_PIXELS = 40; export const WEEKS_PER_TIMESPAN = 4; +export const DAYS_IN_WEEK = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']; diff --git a/src/Day.css b/src/Day.css index 6ea1e9c..ef4e48e 100644 --- a/src/Day.css +++ b/src/Day.css @@ -8,8 +8,18 @@ background-color: rgba(218, 228, 242, 0.2); } +:local(.grayed) { + background-image: linear-gradient(-45deg, #f6f6f6 25%, transparent 25%, transparent 50%, #f6f6f6 50%, #f6f6f6 75%, transparent 75%, transparent); + background-size: 10px 10px; + border-bottom: none; +} + +:local(.block) { + position: absolute; + width: 100%; +} + :local(.mouseTarget) { - top: 0; left: 0; bottom: 0; right: 0; diff --git a/src/Day.jsx b/src/Day.jsx index f901e84..cad8cd2 100644 --- a/src/Day.jsx +++ b/src/Day.jsx @@ -46,7 +46,8 @@ export default class Day extends PureComponent { relativeY(pageY, rounding = ROUND_TO_NEAREST_MINS) { const { top } = this.mouseTargetRef.getBoundingClientRect(); - const realY = pageY - top - document.body.scrollTop; + let realY = pageY - top - document.body.scrollTop; + realY += this.props.hourLimits.top; // offset top blocker const snapTo = (rounding / 60) * HOUR_IN_PIXELS; return Math.floor(realY / snapTo) * snapTo; } @@ -67,7 +68,7 @@ export default class Day extends PureComponent { }); } - handleItemModification(edge, { start, end }, { pageY }) { + handleItemModification(edge, { start, end }, { pageY, currentTarget }) { const position = this.relativeY(pageY); this.setState(({ selections }) => { for (let i = 0; i < selections.length; i++) { @@ -77,6 +78,7 @@ export default class Day extends PureComponent { index: i, lastKnownPosition: position, minLengthInMinutes: 30, + target: currentTarget, }; } } @@ -137,24 +139,44 @@ export default class Day extends PureComponent { })); } + // eslint-disable-next-line class-methods-use-this + hasReachedTop({ offsetTop }) { + const { hourLimits } = this.props; + return offsetTop <= hourLimits.top; + } + + hasReachedBottom({ offsetTop, offsetHeight }) { + const { hourLimits } = this.props; + return (offsetTop + offsetHeight) >= hourLimits.bottom; + } handleMouseMove({ pageY }) { if (typeof this.state.index === 'undefined') { return; } const { date, timeZone } = this.props; const position = this.relativeY(pageY); - this.setState(({ minLengthInMinutes, selections, edge, index, lastKnownPosition }) => { + this.setState(({ minLengthInMinutes, selections, edge, index, lastKnownPosition, target }) => { const selection = selections[index]; let newMinLength = minLengthInMinutes; if (edge === 'both') { // move element const diff = toDate(date, position, timeZone).getTime() - toDate(date, lastKnownPosition, timeZone).getTime(); - const newStart = new Date(selection.start.getTime() + diff); - const newEnd = new Date(selection.end.getTime() + diff); + let newStart = new Date(selection.start.getTime() + diff); + let newEnd = new Date(selection.end.getTime() + diff); if (hasOverlap(selections, newStart, newEnd, index)) { return {}; } + if (this.hasReachedTop(target) && diff < 0) { + // if has reached top blocker and it is going upwards, fix the newStart. + newStart = selection.start; + } + + if (this.hasReachedBottom(target) && diff > 0) { + // if has reached bottom blocker and it is going downwards, fix. + newEnd = selection.end; + } + selection.start = newStart; selection.end = newEnd; } else { @@ -195,17 +217,23 @@ export default class Day extends PureComponent { render() { const { + available, availableWidth, date, events, timeConvention, timeZone, touchToDeleteSelection, + hourLimits, } = this.props; const { selections, index } = this.state; - const classes = [styles.component]; + + if (!available) { + classes.push(styles.grayed); + } + if (inSameDay(date, new Date(), timeZone)) { classes.push(styles.today); } @@ -218,6 +246,20 @@ export default class Day extends PureComponent { width: availableWidth, }} > +
+
{events.map(({ allDay, start, @@ -240,17 +282,23 @@ export default class Day extends PureComponent { frozen /> ))} -
+ { available && ( +
+ )} {selections.map(({ start, end }, i) => ( -
+
{!hideDates && this.text()} {hideDates && this.dateLessText()}
@@ -71,4 +76,5 @@ DayHeader.propTypes = { availableWidth: PropTypes.number, events: PropTypes.arrayOf(PropTypes.object), hideDates: PropTypes.bool, + available: PropTypes.bool, }; diff --git a/src/Validators.js b/src/Validators.js new file mode 100644 index 0000000..2b09021 --- /dev/null +++ b/src/Validators.js @@ -0,0 +1,10 @@ +import { DAYS_IN_WEEK } from './Constants'; + +module.exports = { + validateDays(propValue, key, componentName, location, propFullName) { + if (!DAYS_IN_WEEK.includes(propValue[key])) { + return new Error(`Invalid prop ${propFullName} supplied to ${componentName}. Validation failed.`); + } + return true; + }, +}; diff --git a/src/Week.jsx b/src/Week.jsx index 4613f00..13e24e9 100644 --- a/src/Week.jsx +++ b/src/Week.jsx @@ -2,7 +2,8 @@ import PropTypes from 'prop-types'; import React, { PureComponent } from 'react'; import momentTimezone from 'moment-timezone'; -import { HOUR_IN_PIXELS, RULER_WIDTH_IN_PIXELS } from './Constants'; +import { HOUR_IN_PIXELS, RULER_WIDTH_IN_PIXELS, MINUTE_IN_PIXELS } from './Constants'; +import { validateDays } from './Validators'; import Day from './Day'; import DayHeader from './DayHeader'; import Ruler from './Ruler'; @@ -106,6 +107,16 @@ export default class Week extends PureComponent { }); } + // generate the props required for Day to block specific hours. + generateHourLimits() { + const { availableHourRange } = this.props; + return { top: availableHourRange.start * HOUR_IN_PIXELS, // top blocker + bottom: availableHourRange.end * HOUR_IN_PIXELS, + bottomHeight: (24 - availableHourRange.end) * HOUR_IN_PIXELS, // bottom height + difference: ((availableHourRange.end - availableHourRange.start) * HOUR_IN_PIXELS) + (MINUTE_IN_PIXELS * 14), + }; + } + // eslint-disable-next-line class-methods-use-this renderLines() { const result = []; @@ -131,10 +142,15 @@ export default class Week extends PureComponent { timeZone, recurring, touchToDeleteSelection, + availableDays, } = this.props; - const { dayEvents, daySelections, daysWidth, widthOfAScrollbar } = this.state; + const filteredDays = week.days.map((day) => { + const updatedDay = day; + updatedDay.available = availableDays.includes(day.name.toLowerCase()); + return updatedDay; + }); return (
All-day
- {week.days.map((day, i) => ( + {filteredDays.map((day, i) => ( ))}
@@ -178,8 +195,9 @@ export default class Week extends PureComponent { ref={this.handleDaysRef} > - {week.days.map((day, i) => ( + {filteredDays.map((day, i) => ( ))} @@ -221,4 +240,9 @@ Week.propTypes = { week: PropTypes.object.isRequired, recurring: PropTypes.bool, touchToDeleteSelection: PropTypes.bool, + availableDays: PropTypes.arrayOf(validateDays), + availableHourRange: PropTypes.shape({ + start: PropTypes.number, + end: PropTypes.number, + }).isRequired, }; diff --git a/src/test.jsx b/src/test.jsx index 22c5a6a..4b474cf 100644 --- a/src/test.jsx +++ b/src/test.jsx @@ -155,6 +155,8 @@ class Test extends Component { initialSelections={initialSelections} onEventsRequested={this.handleEventsRequested} recurring={recurring} + availableDays={['monday', 'tuesday', 'wednesday', 'thursday', 'friday']} + availableHourRange={{ start: 6, end: 20 }} />