UNPKG

5.65 kBTypeScriptView Raw
1/*
2 * Copyright 2015 Palantir Technologies, Inc. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import * as React from "react";
18import { CaptionElementProps } from "react-day-picker";
19
20import { AbstractPureComponent2, Divider, HTMLSelect, IconSize, OptionProps } from "@blueprintjs/core";
21
22import * as Classes from "./common/classes";
23import { clone } from "./common/dateUtils";
24import { measureTextWidth } from "./common/utils";
25
26export interface IDatePickerCaptionProps extends CaptionElementProps {
27 maxDate: Date;
28 minDate: Date;
29 onMonthChange?: (month: number) => void;
30 onYearChange?: (year: number) => void;
31 /** Callback invoked when the month or year `<select>` is changed. */
32 onDateChange?: (date: Date) => void;
33 reverseMonthAndYearMenus?: boolean;
34}
35
36export interface IDatePickerCaptionState {
37 monthRightOffset: number;
38}
39
40export 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 // build the list of available months, limiting based on minDate and maxDate as necessary
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 // ignore change events with invalid values to prevent crash on iOS Safari (#4178)
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}