Skip to content
This repository was archived by the owner on Jun 11, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
/>
```

Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": "[email protected]:trotzig/react-available-times.git",
Expand Down
14 changes: 13 additions & 1 deletion src/AvailableTimes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -216,6 +217,8 @@ export default class AvailableTimes extends PureComponent {
timeZone,
recurring,
touchToDeleteSelection,
availableDays,
availableHourRange,
} = this.props;

const {
Expand Down Expand Up @@ -298,6 +301,8 @@ export default class AvailableTimes extends PureComponent {
height={height}
recurring={recurring}
touchToDeleteSelection={touchToDeleteSelection}
availableDays={availableDays}
availableHourRange={availableHourRange}
/>
);
})}
Expand Down Expand Up @@ -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 },
};
1 change: 1 addition & 0 deletions src/Constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
12 changes: 11 additions & 1 deletion src/Day.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
89 changes: 72 additions & 17 deletions src/Day.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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++) {
Expand All @@ -77,6 +78,7 @@ export default class Day extends PureComponent {
index: i,
lastKnownPosition: position,
minLengthInMinutes: 30,
target: currentTarget,
};
}
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
Expand All @@ -218,6 +246,20 @@ export default class Day extends PureComponent {
width: availableWidth,
}}
>
<div
className={`${styles.grayed} ${styles.block}`}
style={{
height: hourLimits.top,
top: 0,
}}
/>
<div
className={`${styles.grayed} ${styles.block}`}
style={{
height: hourLimits.bottomHeight,
top: hourLimits.bottom,
}}
/>
{events.map(({
allDay,
start,
Expand All @@ -240,17 +282,23 @@ export default class Day extends PureComponent {
frozen
/>
))}
<div
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
onMouseMove={this.handleMouseMove}
onMouseOut={this.handleMouseUp}
onTouchStart={this.handleTouchStart}
onTouchMove={this.handleTouchMove}
onTouchEnd={this.handleTouchEnd}
className={styles.mouseTarget}
ref={this.handleMouseTargetRef}
/>
{ available && (
<div
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
onMouseMove={this.handleMouseMove}
onMouseOut={this.handleMouseUp}
onTouchStart={this.handleTouchStart}
onTouchMove={this.handleTouchMove}
onTouchEnd={this.handleTouchEnd}
className={styles.mouseTarget}
ref={this.handleMouseTargetRef}
style={{
top: hourLimits.top,
height: hourLimits.difference,
}}
/>
)}
{selections.map(({ start, end }, i) => (
<TimeSlot
// eslint-disable-next-line react/no-array-index-key
Expand All @@ -273,7 +321,14 @@ export default class Day extends PureComponent {
}

Day.propTypes = {
available: PropTypes.bool,
availableWidth: PropTypes.number.isRequired,
hourLimits: PropTypes.shape({
top: PropTypes.number,
bottom: PropTypes.number,
bottomHeight: PropTypes.number,
difference: PropTypes.number,
}).isRequired,
timeConvention: PropTypes.oneOf(['12h', '24h']),
timeZone: PropTypes.string.isRequired,

Expand Down
4 changes: 4 additions & 0 deletions src/DayHeader.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
text-align: center;
}

:local(.transparent) {
opacity: 0.25;
}

:local(.events) {
flex-grow: 1;
text-align: left;
Expand Down
8 changes: 7 additions & 1 deletion src/DayHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ export default class DayHeader extends Component {
events,
hideDates,
} = this.props;
const classes = [styles.day];

if (!this.props.available) {
classes.push(styles.transparent);
}

return (
<div
Expand All @@ -44,7 +49,7 @@ export default class DayHeader extends Component {
width: availableWidth,
}}
>
<div className={styles.day}>
<div className={classes.join(' ')}>
{!hideDates && this.text()}
{hideDates && this.dateLessText()}
</div>
Expand All @@ -71,4 +76,5 @@ DayHeader.propTypes = {
availableWidth: PropTypes.number,
events: PropTypes.arrayOf(PropTypes.object),
hideDates: PropTypes.bool,
available: PropTypes.bool,
};
10 changes: 10 additions & 0 deletions src/Validators.js
Original file line number Diff line number Diff line change
@@ -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;
},
};
Loading