1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | import * as React from "react";
|
18 | import { CaptionElementProps } from "react-day-picker";
|
19 |
|
20 | import { AbstractPureComponent2, Divider, HTMLSelect, IconSize, OptionProps } from "@blueprintjs/core";
|
21 |
|
22 | import * as Classes from "./common/classes";
|
23 | import { clone } from "./common/dateUtils";
|
24 | import { measureTextWidth } from "./common/utils";
|
25 |
|
26 | export interface IDatePickerCaptionProps extends CaptionElementProps {
|
27 | maxDate: Date;
|
28 | minDate: Date;
|
29 | onMonthChange?: (month: number) => void;
|
30 | onYearChange?: (year: number) => void;
|
31 |
|
32 | onDateChange?: (date: Date) => void;
|
33 | reverseMonthAndYearMenus?: boolean;
|
34 | }
|
35 |
|
36 | export interface IDatePickerCaptionState {
|
37 | monthRightOffset: number;
|
38 | }
|
39 |
|
40 | export class DatePickerCaption extends AbstractPureComponent2<IDatePickerCaptionProps, IDatePickerCaptionState> {
|
41 | public state: IDatePickerCaptionState = { monthRightOffset: 0 };
|
42 |
|
43 | private containerElement: HTMLElement;
|
44 |
|
45 | private displayedMonthText: string;
|
46 |
|
47 | private handleMonthSelectChange = this.dateChangeHandler((d, month) => d.setMonth(month), this.props.onMonthChange);
|
48 |
|
49 | private handleYearSelectChange = this.dateChangeHandler((d, year) => d.setFullYear(year), this.props.onYearChange);
|
50 |
|
51 | public render() {
|
52 | const { date, locale, localeUtils, minDate, maxDate, months = localeUtils.getMonths(locale) } = this.props;
|
53 | const minYear = minDate.getFullYear();
|
54 | const maxYear = maxDate.getFullYear();
|
55 | const displayMonth = date.getMonth();
|
56 | const displayYear = date.getFullYear();
|
57 |
|
58 |
|
59 | const startMonth = displayYear === minYear ? minDate.getMonth() : 0;
|
60 | const endMonth = displayYear === maxYear ? maxDate.getMonth() + 1 : undefined;
|
61 | const monthOptionElements = months
|
62 | .map<OptionProps>((month, i) => ({ label: month, value: i }))
|
63 | .slice(startMonth, endMonth);
|
64 |
|
65 | const years: Array<number | OptionProps> = [minYear];
|
66 | for (let year = minYear + 1; year <= maxYear; ++year) {
|
67 | years.push(year);
|
68 | }
|
69 | // allow out-of-bounds years but disable the option. this handles the Dec 2016 case in #391.
|
70 | if (displayYear > maxYear) {
|
71 | years.push({ value: displayYear, disabled: true });
|
72 | }
|
73 |
|
74 | this.displayedMonthText = months[displayMonth];
|
75 |
|
76 | const monthSelect = (
|
77 | <HTMLSelect
|
78 | iconProps={{ style: { right: this.state.monthRightOffset } }}
|
79 | className={Classes.DATEPICKER_MONTH_SELECT}
|
80 | key="month"
|
81 | minimal={true}
|
82 | onChange={this.handleMonthSelectChange}
|
83 | value={displayMonth}
|
84 | options={monthOptionElements}
|
85 | />
|
86 | );
|
87 | const yearSelect = (
|
88 | <HTMLSelect
|
89 | className={Classes.DATEPICKER_YEAR_SELECT}
|
90 | key="year"
|
91 | minimal={true}
|
92 | onChange={this.handleYearSelectChange}
|
93 | value={displayYear}
|
94 | options={years}
|
95 | />
|
96 | );
|
97 |
|
98 | const orderedSelects = this.props.reverseMonthAndYearMenus
|
99 | ? [yearSelect, monthSelect]
|
100 | : [monthSelect, yearSelect];
|
101 |
|
102 | return (
|
103 | <div className={this.props.classNames.caption}>
|
104 | <div className={Classes.DATEPICKER_CAPTION} ref={ref => (this.containerElement = ref)}>
|
105 | {orderedSelects}
|
106 | </div>
|
107 | <Divider />
|
108 | </div>
|
109 | );
|
110 | }
|
111 |
|
112 | public componentDidMount() {
|
113 | this.requestAnimationFrame(() => this.positionArrows());
|
114 | }
|
115 |
|
116 | public componentDidUpdate() {
|
117 | this.positionArrows();
|
118 | }
|
119 |
|
120 | private positionArrows() {
|
121 | // measure width of text as rendered inside our container element.
|
122 | const monthTextWidth = measureTextWidth(
|
123 | this.displayedMonthText,
|
124 | Classes.DATEPICKER_CAPTION_MEASURE,
|
125 | this.containerElement,
|
126 | );
|
127 | const monthSelectWidth =
|
128 | this.containerElement == null ? 0 : this.containerElement.firstElementChild.clientWidth;
|
129 | const rightOffset = Math.max(2, monthSelectWidth - monthTextWidth - IconSize.STANDARD - 2);
|
130 | this.setState({ monthRightOffset: rightOffset });
|
131 | }
|
132 |
|
133 | private dateChangeHandler(updater: (date: Date, value: number) => void, handler?: (value: number) => void) {
|
134 | return (e: React.FormEvent<HTMLSelectElement>) => {
|
135 | const value = parseInt((e.target as HTMLSelectElement).value, 10);
|
136 |
|
137 | if (isNaN(value)) {
|
138 | return;
|
139 | }
|
140 | const newDate = clone(this.props.date);
|
141 | updater(newDate, value);
|
142 | this.props.onDateChange?.(newDate);
|
143 | handler?.(value);
|
144 | };
|
145 | }
|
146 | }
|