父级中有一个名为visibleMonth
的函数,该函数返回一个对象数组,该对象数组显示在视口中可见的月份。我已经成功地将一个名为getVisibleMonth的道具从父项[Calendar
]传递给其子项[CalendarBarChart
]
我现在想要实现的是分配一个css类来显示当前在视口中的月份,所以像someVisibleMonths(可以通过this.getVisibileMonth()属性实现,并以正确的格式获取月份,它将是{{1}中的month.start.format('MMMM')
)=== month
,然后将类分配给该范围。我有点困惑如何去做。
这是我的父组件,我知道它很长!但重点将放在averageByMonth
上:
visibleMonth
这是子组件:
import React, { Component, Fragment } from 'react';
import filter from 'lodash/filter';
import find from 'lodash/find';
import flatten from 'lodash/flatten';
import some from 'lodash/some';
import isEqual from 'lodash/isEqual';
import cx from 'classnames';
import moment from 'utils/moment/moment';
import ScrollWatcher from 'utils/ScrollWatcher';
import CalendarDay from './CalendarDay';
import CalendarMonth from './CalendarMonth';
import CalendarBarChart from './CalendarBarChart';
import css from './Calendar.css';
import type { BookableMonth, DayState, Period } from './types';
import { startOfMonth } from './utils';
import { Icon } from '@appearhere/bloom';
type Props = {
direction: 'row' | 'column',
mask: boolean,
maskSize: number,
minimumDurationInDays: number,
months: Array<BookableMonth>,
onSelectionUpdate: (Array<moment>) => mixed,
selectedDates: Array<Period>,
sparse: boolean,
};
type State = {
currentMonth: number,
startOn?: moment,
endOn?: moment,
endHighlight?: moment,
};
export default class Calendar extends Component<Props, State> {
static defaultProps = {
sparse: false,
mask: false,
maskSize: 3,
direction: 'row',
selectedDates: [],
};
state = {
currentMonth: 0,
startOn: null,
endOn: null,
endHighlight: null,
};
dynamicBarChartContainerRef: any = React.createRef();
barChartContainerRef: any = React.createRef();
scrollTriggeredByClickingOnLink: boolean = false;
calendarScrollWatcher = new ScrollWatcher({
onScrollUp: scrollState => {
this.scrollTriggeredByClickingOnLink = false;
const barChartContainer = this.barChartContainerRef.current;
const dynamicBarChartContainer = this.dynamicBarChartContainerRef.current;
// Let the dynamic chart disappear when we reach the top and make sure the
// static chart is visible.
if (scrollState.enteringTop) {
// Make sure the static chart is visible.
barChartContainer.classList.remove(css.invisible);
// Make sure the dynamic chart is invisible during repositioning.
dynamicBarChartContainer.classList.add(css.invisible);
// Set the dynamic chart to the scrolled-up position instantly, skipping the transition,
// as read at https://medium.com/building-blocks/how-to-skip-css-transitions-with-jquery-e0155d06e82e
dynamicBarChartContainer.classList.remove(css.dynamicBarChartContainerTransition);
dynamicBarChartContainer.classList.remove(css.dynamicBarChartVisiblePosition);
dynamicBarChartContainer.offsetHeight; // Check the link above for why this is necessary.
dynamicBarChartContainer.classList.add(css.dynamicBarChartContainerTransition);
// Repositioning is over. Make the dynamic chart visible again.
dynamicBarChartContainer.classList.remove(css.invisible);
// If the static chart is already out of the viewport, slide in the dynamic chart.
} else if (scrollState.scrollTop > dynamicBarChartContainer.offsetHeight) {
dynamicBarChartContainer.classList.add(css.dynamicBarChartVisiblePosition);
}
},
onScrollDown: () => {
const dynamicBarChartContainer = this.dynamicBarChartContainerRef.current;
// If this is not a regular scroll but we're jumping to a certain
// location using a link, ensure the dynamic bar chart is visible.
if (this.scrollTriggeredByClickingOnLink) {
dynamicBarChartContainer.classList.add(css.dynamicBarChartVisiblePosition);
this.scrollTriggeredByClickingOnLink = false;
// Otherwise hide the dynamic bar chart.
} else {
dynamicBarChartContainer.classList.remove(css.dynamicBarChartVisiblePosition);
}
},
});
componentWillReceiveProps(newProps: Props) {
const { selectedDates } = newProps;
if (!isEqual(selectedDates, this.props.selectedDates)) {
this.setDatesState(newProps);
}
}
componentDidMount() {
this.setDatesState(this.props);
}
setDatesState = (props: Props) => {
const { selectedDates, sparse } = props;
if (sparse) return;
let startOn = null;
let endOn = null;
if (selectedDates[0]) {
startOn = selectedDates[0].startOn;
endOn = selectedDates[0].endOn;
}
this.setState({
startOn,
endOn,
endHighlight: endOn || startOn,
});
};
goToPrev = () =>
this.canGoToPrev() && this.setState({ currentMonth: this.state.currentMonth - 1 });
goToNext = () =>
this.canGoToNext() && this.setState({ currentMonth: this.state.currentMonth + 1 });
visibleMonths = (): Array<BookableMonth> => {
const { mask, maskSize, months } = this.props;
const { currentMonth } = this.state;
if (mask) {
return months.slice(currentMonth, currentMonth + maskSize);
}
return months;
};
toPeriod = (startOn: moment, endOn?: moment): Period => ({
startOn,
endOn: endOn || null,
});
updateSparseDates = (selectedDates: Array<Period>, date: moment): Array<Period> => {
if (this.isSelected(date)) return filter(selectedDates, ({ startOn }) => !startOn.isSame(date));
return [...selectedDates, this.toPeriod(date, date)];
};
updateConsecutiveDates = (selectedDates: Array<Period>, date: moment): Array<Period> => {
let startOn;
let endOn;
if (selectedDates[0]) ({ startOn, endOn } = selectedDates[0]);
if (startOn && !endOn && !date.isBefore(startOn)) return [this.toPeriod(startOn, date)];
return [this.toPeriod(date)];
};
handleInteraction = (e: any, date: moment): void => {
const { onSelectionUpdate, selectedDates, sparse } = this.props;
if (this.isMinimumBooking(date) && !this.isLastMinimum(date) && !this.state.endOn) return;
if (sparse) onSelectionUpdate(this.updateSparseDates(selectedDates, date));
else onSelectionUpdate(this.updateConsecutiveDates(selectedDates, date));
};
handleHighlight = (date: moment): void => {
if (this.props.sparse) return;
const { startOn, endOn } = this.state;
if (startOn && endOn) {
this.setState({
endHighlight: null,
});
}
if (startOn && !endOn) {
if (!this.isMinimumBooking(date) || this.isLastMinimum(date)) {
this.setState({
endHighlight: moment.max(date, startOn),
});
} else {
this.setState({
endHighlight: startOn,
});
}
}
};
isDisabled = (date: moment): boolean => {
const month = find(this.props.months, m => m.start.isSame(startOfMonth(date)));
if (month) {
const monthDays = flatten(month.weeks);
return find(monthDays, d => d.date.isSame(date)).disabled;
}
return true;
};
isMinimumBooking = (date: moment): boolean => {
const { startOn } = this.state;
if (startOn) {
return date.isBetween(
startOn,
startOn.clone().add(this.props.minimumDurationInDays - 1, 'days'),
null,
'[]',
);
}
return false;
};
isLastMinimum = (date: moment): boolean =>
!!this.state.startOn &&
date.isSame(this.state.startOn.clone().add(this.props.minimumDurationInDays - 1, 'days'));
isSelected = (date: moment): boolean =>
some(this.props.selectedDates, ({ startOn, endOn }: Period) => {
if (startOn && endOn) return date.isBetween(startOn, endOn, null, '[]');
return date.isSame(startOn);
});
isFirstSelected = (date: moment): boolean =>
some(this.props.selectedDates, ({ startOn }: Period) => date.isSame(startOn, 'day'));
isLastSelected = (date: moment): boolean =>
some(this.props.selectedDates, ({ endOn }: Period) => date.isSame(endOn, 'day'));
isHighlighted = (date: moment): boolean =>
!!this.state.startOn &&
!!this.state.endHighlight &&
!this.state.endOn &&
date.isBetween(this.state.startOn, this.state.endHighlight, null, '[]');
isLastHighlighted = (date: moment): boolean =>
date.isSame(this.state.endHighlight, 'day') ||
(!!this.state.startOn &&
!this.state.endOn &&
!this.state.endHighlight &&
date.isSame(this.state.startOn, 'day'));
getDayState = (date: moment): DayState => ({
isDisabled: this.isDisabled(date),
isSelected: this.isSelected(date),
isFirstSelected: this.isFirstSelected(date),
isLastSelected: this.isLastSelected(date),
isHighlighted: this.isHighlighted(date),
isFirstHighlighted: false,
isLastHighlighted: this.isLastHighlighted(date),
isMinimumBooking: this.isMinimumBooking(date),
isLastMinimum: this.isLastMinimum(date),
});
canGoToPrev = (): boolean => this.state.currentMonth > 0;
canGoToNext = (): boolean => {
const { maskSize, months } = this.props;
const { currentMonth } = this.state;
return currentMonth < months.length - maskSize;
};
handleCalendarScroll = (event: SyntheticEvent<HTMLButtonElement>): void => {
this.calendarScrollWatcher.registerScrollEvent(event);
};
goToMonth = (month: number): void => {
const { mask } = this.props;
if (mask) {
this.setState({ currentMonth: month });
} else {
this.scrollTriggeredByClickingOnLink = true;
const monthId = `Calendar-calendar-month-${month}-container`;
const calendarMonthContainer = document.getElementById(monthId);
const calendarContainer = document.getElementById('Calendar-calendar-container');
const dynamicBarChartContainer = this.dynamicBarChartContainerRef.current;
if (!calendarMonthContainer || !calendarContainer || !dynamicBarChartContainer) {
return;
}
const newScrollTop = calendarMonthContainer.offsetTop - dynamicBarChartContainer.offsetHeight;
calendarContainer.scrollTop = newScrollTop;
}
};
render() {
console.log(this.visibleMonths());
const { mask, direction } = this.props;
const calendarBarChart = (
<CalendarBarChart
getVisibleMonth={this.visibleMonths}
months={this.props.months}
onClickOnMonth={this.goToMonth}
/>
);
return (
<div className={css.calendarWrapper}>
<div
id="Calendar-calendar-container"
className={css.root}
onScroll={this.handleCalendarScroll}
>
<div ref={this.barChartContainerRef}>{calendarBarChart}</div>
<div className={css.arrowButtons}>
{mask && (
<Fragment>
<span
data-testid="calendar-prev-month-button"
onClick={this.goToPrev}
className={cx(css.prev, !this.canGoToPrev() && css.arrowDisabled)}
>
<Icon name="chevron" className={css.prevIcon} />
</span>
<span
data-testid="calendar-next-month-button"
onClick={this.goToNext}
className={cx(css.next, !this.canGoToNext() && css.arrowDisabled)}
>
<Icon name="chevron" className={css.nextIcon} />
</span>
</Fragment>
)}
</div>
<div
className={cx(
css.monthsWrapper,
this.props.direction === 'column'
? css.monthsWrapperVertical
: css.monthsWrapperHorizontal,
)}
>
{this.visibleMonths().map((month, index) => (
<div
key={month.start.format('YYYYMM')}
className={css.monthWrapper}
id={`Calendar-calendar-month-${index}-container`}
>
<CalendarMonth
month={month}
onHighlight={this.handleHighlight}
onInteraction={this.handleInteraction}
getDayState={this.getDayState}
DayComponent={CalendarDay}
direction={direction}
/>
</div>
))}
</div>
</div>
<div
ref={this.dynamicBarChartContainerRef}
className={cx(css.dynamicBarChartContainer, css.dynamicBarChartContainerTransition)}
>
<div className={css.dynamicBarChart}>{calendarBarChart}</div>
</div>
</div>
);
}
}
我可以通过执行以下操作来获取视口中当前显示的月份:
// @flow
import React, { Component } from 'react';
import moment from 'utils/moment/moment';
import i18n from 'utils/i18n/i18n';
import css from './CalendarBarChart.css';
import type { BookableMonth } from './types';
import cx from 'classnames';
import flatten from 'lodash/flatten';
const t = i18n.withPrefix('client.apps.space');
type Props = {
months: Array<BookableMonth>,
numberOfDisplayableMonths: number,
minPercentage: number,
remainingPercentage: number,
onClickOnMonth: (month: number) => void,
getVisibleMonth: () => Array<BookableMonth>,
};
export default class CalendarBarChart extends Component<Props> {
static defaultProps = {
numberOfDisplayableMonths: 12,
minPercentage: 20,
remainingPercentage: 80,
};
onClickOnMonth = (event: SyntheticEvent<*>): void => {
const { onClickOnMonth } = this.props;
if (!onClickOnMonth) return;
const dataMonth = event.currentTarget.getAttribute('data-month');
const month = parseInt(dataMonth, 10);
onClickOnMonth(month);
};
convertToAverageByMonth = (months: Array<BookableMonth>): { [key: string]: number } =>
months.slice(0, this.props.numberOfDisplayableMonths).reduce(this.averageMonths, {});
averageMonths = (accumulator: Object, month: BookableMonth): { [key: string]: number } => {
/* eslint-disable no-param-reassign */
const today = moment();
const currentMonth = month.start.month();
const validDays = flatten(month.weeks).filter(
day => currentMonth === day.date.month() && day.date.isSameOrAfter(today, 'day'),
);
accumulator[month.start.format('MMM')] =
validDays.reduce((average, day) => average + day.price.amount, 0) / validDays.length;
return accumulator;
};
barPercentage = (month: string, averageByMonth: Object) => {
const averages: Array<number> = Object.values(averageByMonth);
const min = Math.min(...averages);
const max = Math.max(...averages);
if (min === max) return this.props.minPercentage + this.props.remainingPercentage;
const difference = max - min;
const monthAverage = averageByMonth[month];
return (
this.props.minPercentage + (monthAverage - min) / difference * this.props.remainingPercentage
);
};
isDynamic = (lowestPrice: number, averageByMonth: Object) =>
!Object.values(averageByMonth).every(price => price === lowestPrice);
render() {
const averageByMonth = this.convertToAverageByMonth(this.props.months);
const averages: Array<number> = Object.values(averageByMonth);
const lowestPrice = Math.min(...averages);
const dynamicPricing = this.isDynamic(lowestPrice, averageByMonth);
return dynamicPricing ? (
<div>
<ul className={css.chart}>
{Object.keys(averageByMonth).map((month, index) => (
<li
className={css.barPosition}
key={month}
data-month={index}
onClick={this.onClickOnMonth}
>
<span
style={{ height: `${this.barPercentage(month, averageByMonth)}%` }}
className={cx(css.bar, lowestPrice === averageByMonth[month] && css.cheapestBar)}
title={month}
/>
</li>
))}
</ul>
<div className={css.chartLegend}>
<div className={css.bulletPoint} />
<span>{t('calendar_modal.lowest_price')}</span>
</div>
</div>
) : null;
}
}
但是我该如何使用它并检查跨度中的{this.props.getVisibleMonth().map(month => <li>{month.start.format('MMMM')}</li>)}
并在{month}
时分配css