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) => (