, data?: any, cb?: () => void) => void;
76 | /** Whether to display previous page button as active or disabled. */
77 | hasPrevPage?: boolean;
78 | /** Whether to display next page button as active or disabled. */
79 | hasNextPage?: boolean;
80 | /** A value (date, year or anything like that) that is displayed in calendar header. */
81 | currentHeadingValue?: string;
82 | /** Called after click on calendar header. */
83 | onHeaderClick?: () => void;
84 | }
85 |
86 | export interface OnValueClickData {
87 | [key: string]: any;
88 | /** Position of the clicked cell. */
89 | itemPosition: number;
90 | /** Text content of the clicked cell. */
91 | value: string;
92 | }
93 |
94 | /** Base class for picker view components. */
95 | class BaseCalendarView extends React.Component
{
96 | protected calendarNode: HTMLElement | undefined;
97 |
98 | public componentDidMount() {
99 | if (this.props.onMount) {
100 | this.props.onMount(this.calendarNode);
101 | }
102 | }
103 | }
104 |
105 | export default BaseCalendarView;
106 |
--------------------------------------------------------------------------------
/src/views/CalendarBody/Body.tsx:
--------------------------------------------------------------------------------
1 | import isNil from 'lodash/isNil';
2 | import isArray from 'lodash/isArray';
3 |
4 | import * as React from 'react';
5 | import { Table } from 'semantic-ui-react';
6 |
7 | import { OnValueClickData } from '../BaseCalendarView';
8 | import Cell from './Cell';
9 | import {
10 | cellStyleWidth3,
11 | cellStyleWidth4,
12 | cellStyleWidth7,
13 | CellWidthStyle,
14 | } from './Cell';
15 |
16 | export type BodyWidth = 3 | 4 | 7;
17 |
18 | interface BodyProps {
19 | /** A number of columns in a row. */
20 | width: BodyWidth;
21 | /** Data that is used to fill a calendar. */
22 | data: string[];
23 | /** Called after a click on calendar's cell. */
24 | onCellClick: (e: React.SyntheticEvent, data: OnValueClickData) => void;
25 | /** Called on cell hover. */
26 | onCellHover: (e: React.SyntheticEvent, data: any) => void;
27 | /** Index of an element in `data` array that should be displayed as hovered. */
28 | hovered?: number;
29 | /** Index of an element (or array of indexes) in `data` array that should be displayed as active. */
30 | active?: number | number[];
31 | /** Array of element indexes in `data` array that should be displayed as disabled. */
32 | disabled?: number[];
33 | /** Array of element indexes in `data` array that should be displayed as marked. */
34 | marked?: number[];
35 | /** The color of the mark that will be displayed on the calendar. */
36 | markColor?: string;
37 | }
38 |
39 | function Body(props: BodyProps) {
40 | const {
41 | data,
42 | width,
43 | onCellClick,
44 | active,
45 | disabled,
46 | hovered,
47 | onCellHover,
48 | marked,
49 | markColor,
50 | } = props;
51 | const content = buildRows(data, width).map((row, rowIndex) => (
52 |
53 | { row.map((item, itemIndex) => (
54 | |
66 | )) }
67 |
68 | ));
69 |
70 | return (
71 |
72 | { content }
73 |
74 | );
75 | }
76 |
77 | function buildRows(data: string[], width: number): string[][] {
78 | const height = data.length / width;
79 | const rows = [];
80 | for (let i = 0; i < height; i++) {
81 | rows.push(data.slice((i * width), (i * width) + width));
82 | }
83 |
84 | return rows;
85 | }
86 |
87 | function isActive(rowIndex: number,
88 | rowWidth: number,
89 | colIndex: number,
90 | active: number | number[]): boolean {
91 | if (isNil(active)) {
92 | return false;
93 | }
94 | if (isArray(active)) {
95 | for (const activeIndex of (active as number[])) {
96 | if (rowIndex * rowWidth + colIndex === activeIndex) {
97 | return true;
98 | }
99 | }
100 | }
101 |
102 | return rowIndex * rowWidth + colIndex === active;
103 | }
104 |
105 | function isHovered(rowIndex: number,
106 | rowWidth: number,
107 | colIndex: number,
108 | hovered: number): boolean {
109 | if (isNil(hovered)) {
110 | return false;
111 | }
112 |
113 | return rowIndex * rowWidth + colIndex === hovered;
114 | }
115 |
116 | function isDisabled(rowIndex: number,
117 | rowWidth: number,
118 | colIndex: number,
119 | disabledIndexes: number[]): boolean {
120 | if (isNil(disabledIndexes) || disabledIndexes.length === 0) {
121 | return false;
122 | }
123 | for (const disabledIndex of disabledIndexes) {
124 | if (rowIndex * rowWidth + colIndex === disabledIndex) {
125 | return true;
126 | }
127 | }
128 |
129 | return false;
130 | }
131 |
132 | function getCellStyle(width: BodyWidth): CellWidthStyle {
133 | switch (width) {
134 | case 3:
135 | return cellStyleWidth3;
136 | case 4:
137 | return cellStyleWidth4;
138 | case 7:
139 | return cellStyleWidth7;
140 | default:
141 | break;
142 | }
143 | }
144 |
145 | function isMarked(rowIndex: number,
146 | rowWidth: number,
147 | colIndex: number,
148 | markedIndexes: number[]): boolean {
149 | if (isNil(markedIndexes) || markedIndexes.length === 0) {
150 | return false;
151 | }
152 | for (const markedIndex of markedIndexes) {
153 | if (rowIndex * rowWidth + colIndex === markedIndex) {
154 | return true;
155 | }
156 | }
157 |
158 | return false;
159 | }
160 |
161 | export default Body;
162 |
--------------------------------------------------------------------------------
/src/inputs/TimeInput.tsx:
--------------------------------------------------------------------------------
1 | import isNil from 'lodash/isNil';
2 | import invoke from 'lodash/invoke';
3 |
4 | import moment from 'moment';
5 | import * as React from 'react';
6 |
7 | import { tick } from '../lib';
8 | import {
9 | BasePickerOnChangeData,
10 | } from '../pickers/BasePicker';
11 | import HourPicker from '../pickers/timePicker/HourPicker';
12 | import MinutePicker from '../pickers/timePicker/MinutePicker';
13 | import InputView from '../views/InputView';
14 | import BaseInput, {
15 | BaseInputProps,
16 | BaseInputPropTypes,
17 | BaseInputState,
18 | MultimodeProps,
19 | MultimodePropTypes,
20 | TimeRelatedProps,
21 | TimeRelatedPropTypes,
22 | } from './BaseInput';
23 | import {
24 | parseValue,
25 | TIME_FORMAT,
26 | buildValue,
27 | } from './parse';
28 |
29 | function getNextMode(currentMode) {
30 | if (currentMode === 'hour') {
31 | return 'minute';
32 | }
33 |
34 | return 'hour';
35 | }
36 |
37 | type CalendarMode = 'hour' | 'minute';
38 |
39 | export type TimeInputProps =
40 | & BaseInputProps
41 | & MultimodeProps
42 | & TimeRelatedProps;
43 |
44 | export type TimeInputOnChangeData = TimeInputProps;
45 |
46 | interface TimeInputState extends BaseInputState {
47 | mode: CalendarMode;
48 | }
49 |
50 | class TimeInput extends BaseInput {
51 | /**
52 | * Component responsibility:
53 | * - parse time input string
54 | * - switch between modes ['hour', 'minute']
55 | * - handle HourPicker/MinutePicker change (format { hour: number, minute: number } into output time string)
56 | */
57 | public static readonly defaultProps = {
58 | ...BaseInput.defaultProps,
59 | icon: 'time',
60 | timeFormat: '24',
61 | disableMinute: false,
62 | };
63 |
64 | public static readonly propTypes = {
65 | ...BaseInputPropTypes,
66 | ...MultimodePropTypes,
67 | ...TimeRelatedPropTypes,
68 | };
69 |
70 | constructor(props) {
71 | super(props);
72 | this.state = {
73 | mode: 'hour',
74 | popupIsClosed: true,
75 | };
76 | }
77 |
78 | public render() {
79 | const {
80 | value,
81 | timeFormat,
82 | closable,
83 | disableMinute,
84 | ...rest
85 | } = this.props;
86 |
87 | return (
88 | this.getPicker()}
96 | />
97 | );
98 | }
99 |
100 | private handleSelect = (
101 | e: React.SyntheticEvent,
102 | { value }: BasePickerOnChangeData,
103 | ) => {
104 |
105 | tick(this.handleSelectUndelayed, e, { value });
106 | }
107 |
108 | private handleSelectUndelayed = (
109 | e: React.SyntheticEvent,
110 | { value }: BasePickerOnChangeData,
111 | ) => {
112 |
113 | const {
114 | hour,
115 | minute,
116 | } = value;
117 | const {
118 | timeFormat,
119 | disableMinute,
120 | } = this.props;
121 |
122 | let outputTimeString = '';
123 | if (this.state.mode === 'hour' && !isNil(hour)) {
124 | outputTimeString = moment({ hour }).format(TIME_FORMAT[timeFormat]);
125 | } else if (!isNil(hour) && !isNil(minute)) {
126 | outputTimeString = moment({ hour, minute }).format(TIME_FORMAT[timeFormat]);
127 | }
128 | invoke(this.props, 'onChange', e, { ...this.props, value: outputTimeString });
129 | if (this.props.closable && (this.state.mode === 'minute' || this.props.disableMinute)) {
130 | this.closePopup();
131 | }
132 | if (!disableMinute) {
133 | this.switchToNextMode();
134 | }
135 | }
136 |
137 | private switchToNextMode = () => {
138 | this.setState(({ mode }) => {
139 | return { mode: getNextMode(mode) };
140 | }, this.onModeSwitch);
141 | }
142 |
143 | private getPicker() {
144 | const {
145 | value,
146 | timeFormat,
147 | inline,
148 | localization,
149 | tabIndex,
150 | pickerStyle,
151 | pickerWidth,
152 | } = this.props;
153 | const currentValue = parseValue(value, TIME_FORMAT[timeFormat], localization);
154 | const pickerProps = {
155 | inline,
156 | onCalendarViewMount: this.onCalendarViewMount,
157 | isPickerInFocus: this.isPickerInFocus,
158 | isTriggerInFocus: this.isTriggerInFocus,
159 | hasHeader: false,
160 | pickerWidth,
161 | pickerStyle,
162 | onHeaderClick: () => undefined,
163 | closePopup: this.closePopup,
164 | initializeWith: buildValue(currentValue, null, localization, TIME_FORMAT[timeFormat]),
165 | value: buildValue(currentValue, null, TIME_FORMAT[timeFormat], localization, null),
166 | onChange: this.handleSelect,
167 | timeFormat,
168 | tabIndex,
169 | localization,
170 | };
171 | if (this.state.mode === 'hour') {
172 | return ;
173 | }
174 |
175 | return ;
176 | }
177 | }
178 |
179 | export default TimeInput;
180 |
--------------------------------------------------------------------------------
/src/inputs/DatesRangeInput.tsx:
--------------------------------------------------------------------------------
1 | import invoke from 'lodash/invoke';
2 | import * as React from 'react';
3 |
4 | import InputView from '../views/InputView';
5 | import {
6 | parseDatesRange,
7 | parseValue,
8 | parseArrayOrValue,
9 | buildValue,
10 | } from './parse';
11 |
12 | import DatesRangePicker, {
13 | DatesRangePickerOnChangeData,
14 | } from '../pickers/dayPicker/DatesRangePicker';
15 | import BaseInput, {
16 | BaseInputProps,
17 | BaseInputPropTypes,
18 | BaseInputState,
19 | DateRelatedProps,
20 | DateRelatedPropTypes,
21 | MinMaxValueProps,
22 | MinMaxValuePropTypes,
23 | MarkedValuesProps,
24 | MarkedValuesPropTypes,
25 | RangeRelatedProps,
26 | RangeRelatedPropTypes,
27 | } from './BaseInput';
28 |
29 | const DATES_SEPARATOR = ' - ';
30 |
31 | export type DatesRangeInputProps =
32 | & BaseInputProps
33 | & DateRelatedProps
34 | & MarkedValuesProps
35 | & MinMaxValueProps
36 | & RangeRelatedProps;
37 |
38 | export type DatesRangeInputOnChangeData = DatesRangeInputProps;
39 |
40 | class DatesRangeInput extends BaseInput {
41 | /**
42 | * Component responsibility:
43 | * - parse input value (start: Moment, end: Moment)
44 | * - handle DayPicker change (format {start: Moment, end: Moment} into
45 | * string 'start - end')
46 | */
47 | public static readonly defaultProps = {
48 | ...BaseInput.defaultProps,
49 | dateFormat: 'DD-MM-YYYY',
50 | icon: 'calendar',
51 | };
52 |
53 | public static readonly propTypes = {
54 | ...BaseInputPropTypes,
55 | ...DateRelatedPropTypes,
56 | ...MarkedValuesPropTypes,
57 | ...MinMaxValuePropTypes,
58 | ...RangeRelatedPropTypes,
59 | };
60 |
61 | constructor(props) {
62 | super(props);
63 | this.state = {
64 | popupIsClosed: true,
65 | };
66 | }
67 |
68 | public render() {
69 | const {
70 | value,
71 | dateFormat,
72 | initialDate,
73 | maxDate,
74 | minDate,
75 | closable,
76 | marked,
77 | markColor,
78 | localization,
79 | allowSameEndDate,
80 | ...rest
81 | } = this.props;
82 |
83 | return (
84 |
93 | );
94 | }
95 |
96 | private getPicker = () => {
97 | const {
98 | value,
99 | dateFormat,
100 | markColor,
101 | marked,
102 | initialDate,
103 | localization,
104 | minDate,
105 | maxDate,
106 | tabIndex,
107 | pickerWidth,
108 | pickerStyle,
109 | allowSameEndDate,
110 | } = this.props;
111 | const {
112 | start,
113 | end,
114 | } = parseDatesRange(value, dateFormat);
115 |
116 | const markedParsed = parseArrayOrValue(marked, dateFormat, localization);
117 | const minDateParsed = parseValue(minDate, dateFormat, localization);
118 | const maxDateParsed = parseValue(maxDate, dateFormat, localization);
119 |
120 | let initializeWith;
121 |
122 | if (!initialDate && minDateParsed || maxDateParsed) {
123 | initializeWith = minDateParsed || maxDateParsed;
124 | } else {
125 | initializeWith = buildValue(start, initialDate, localization, dateFormat);
126 | }
127 |
128 | return (
129 | undefined}
146 | tabIndex={tabIndex}
147 | pickerWidth={pickerWidth}
148 | pickerStyle={pickerStyle}
149 | allowSameEndDate={allowSameEndDate}
150 | />
151 | );
152 | }
153 |
154 | private handleSelect = (e: React.SyntheticEvent,
155 | { value }: DatesRangePickerOnChangeData) => {
156 | const { dateFormat } = this.props;
157 | const {
158 | start,
159 | end,
160 | } = value;
161 | let outputString = '';
162 | if (start && end) {
163 | outputString = `${start.format(dateFormat)}${DATES_SEPARATOR}${end.format(dateFormat)}`;
164 | } else if (start) {
165 | outputString = `${start.format(dateFormat)}${DATES_SEPARATOR}`;
166 | }
167 | invoke(this.props, 'onChange', e, { ...this.props, value: outputString });
168 | if (this.props.closable && start && end) {
169 | this.closePopup();
170 | }
171 | }
172 | }
173 |
174 | export default DatesRangeInput;
175 |
--------------------------------------------------------------------------------
/test/inputs/testParse.js:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import * as _ from 'lodash';
3 | import moment from 'moment';
4 |
5 | import {
6 | getInitializer,
7 | parseValue,
8 | dateValueToString,
9 | } from '../../src/inputs/parse';
10 |
11 | describe('getInitializer', () => {
12 | const dateFormat = 'YYYY-MM-DD HH:mm';
13 |
14 | describe('`dateParams` param provided', () => {
15 | it('return valid moment created from `dateParams`', () => {
16 | const dateParams = {
17 | year: 2018,
18 | month: 4,
19 | date: 15,
20 | hour: 14,
21 | minute: 12,
22 | };
23 | assert(moment.isMoment(getInitializer({ dateFormat, dateParams })), 'return moment');
24 | assert(getInitializer({ dateFormat, dateParams }).isValid(), 'return valid moment');
25 | assert(
26 | getInitializer({ dateFormat, dateParams }).isSame(moment(dateParams), 'minute'),
27 | 'return correct moment');
28 | });
29 | });
30 |
31 | describe('`initialDate` param provided', () => {
32 | it('return valid moment created from `initialDate`', () => {
33 | const initialDate = '2018-05-15 14:12';
34 | assert(moment.isMoment(getInitializer({ initialDate, dateFormat })), 'return moment');
35 | assert(getInitializer({ initialDate, dateFormat }).isValid(), 'return valid moment');
36 | assert(
37 | getInitializer({ initialDate, dateFormat }).isSame(moment(initialDate, dateFormat), 'minute'),
38 | 'return correct moment');
39 | });
40 | });
41 |
42 | describe('`initialDate`, and `dateParams` params provided', () => {
43 | it('return valid moment created from `value`', () => {
44 | const value = '2018-05-15 14:12';
45 | const initialDate = '2020-05-15 15:00';
46 | const dateParams = {
47 | year: 2018,
48 | month: 4,
49 | date: 15,
50 | hour: 14,
51 | minute: 12,
52 | };
53 | assert(moment.isMoment(getInitializer({ initialDate, dateFormat, dateParams })), 'return moment');
54 | assert(getInitializer({ initialDate, dateFormat, dateParams }).isValid(), 'return valid moment');
55 | assert(
56 | getInitializer({ initialDate, dateFormat, dateParams }).isSame(moment(value, dateFormat), 'minute'),
57 | 'return correct moment');
58 | });
59 | });
60 | });
61 |
62 | describe('parseValue', () => {
63 | describe('`value` param provided', () => {
64 | it('create moment from input string', () => {
65 | const value = 'Sep 2015';
66 | const dateFormat = 'MMM YYYY';
67 | const locale = 'en';
68 |
69 | assert(moment.isMoment(parseValue(value, dateFormat, locale)), 'return moment instance');
70 | assert(parseValue(value, dateFormat).isValid(), 'return valid moment instance');
71 | assert(parseValue(value, dateFormat).isSame(moment('Sep 2015', 'MMM YYYY'), 'month'), 'return correct moment');
72 | });
73 |
74 | it('create moment from input Date', () => {
75 | const value = new Date('2015-02-15');
76 | const dateFormat = 'does not matter if value is Date';
77 | const locale = 'en';
78 |
79 | const parsed = parseValue(value, dateFormat, locale);
80 |
81 | assert(moment.isMoment(parsed), 'return moment instance');
82 | assert(parsed.isValid(), 'return valid moment instance');
83 | assert(parsed.isSame(moment('2015-02-15', 'YYYY-MM-DD'), 'date'), 'return correct moment');
84 | });
85 |
86 | it('create moment from input Moment', () => {
87 | const value = moment('2015-02-15', 'YYYY-MM-DD');
88 | const dateFormat = 'does not matter if value is Moment';
89 | const locale = 'en';
90 |
91 | const parsed = parseValue(value, dateFormat, locale);
92 |
93 | assert(moment.isMoment(parsed), 'return moment instance');
94 | assert(parsed.isValid(), 'return valid moment instance');
95 | assert(parsed.isSame(moment('2015-02-15', 'YYYY-MM-DD'), 'date'), 'return correct moment');
96 | });
97 | });
98 |
99 | describe('`value` param is not provided', () => {
100 | it('return undefined', () => {
101 | const dateFormat = 'MMM';
102 | const locale = 'en';
103 |
104 | assert(_.isUndefined(parseValue(undefined, dateFormat, locale)), 'return undefined');
105 | });
106 | });
107 | });
108 |
109 | describe('dateValueToString()', () => {
110 | it('handles string input value', () => {
111 | const inputValue = '17-04-2030';
112 | const dateFormat = 'DD-MM-YYYY';
113 | const locale = 'en';
114 |
115 | const producedValue = dateValueToString(inputValue, dateFormat, locale);
116 |
117 | assert(_.isString(producedValue), 'return string value');
118 | assert.equal(producedValue, inputValue, 'return correct string');
119 | });
120 |
121 | it('handles Date input value', () => {
122 | const inputValue = new Date('2015-08-11');
123 | const dateFormat = 'DD-MM-YYYY';
124 | const locale = 'en';
125 |
126 | const producedValue = dateValueToString(inputValue, dateFormat, locale);
127 |
128 | assert(_.isString(producedValue), 'return string value');
129 | assert.equal(producedValue, '11-08-2015', 'return correct string');
130 | });
131 |
132 | it('handles Moment input value', () => {
133 | const inputValue = moment('2015-08-11', 'YYYY-MM-DD');
134 | const dateFormat = 'DD-MM-YYYY';
135 | const locale = 'en';
136 |
137 | const producedValue = dateValueToString(inputValue, dateFormat, locale);
138 |
139 | assert(_.isString(producedValue), 'return string value');
140 | assert.equal(producedValue, '11-08-2015', 'return correct string');
141 | });
142 | });
143 |
--------------------------------------------------------------------------------
/src/inputs/parse.ts:
--------------------------------------------------------------------------------
1 | import isNil from 'lodash/isNil';
2 | import isArray from 'lodash/isArray';
3 | import isString from 'lodash/isString';
4 | import compact from 'lodash/compact';
5 |
6 | import moment, { Moment } from 'moment';
7 |
8 | export const TIME_FORMAT = {
9 | 24: 'HH:mm',
10 | AMPM: 'hh:mm A',
11 | ampm: 'hh:mm a',
12 | };
13 |
14 | type ParseValueData =
15 | | string
16 | | moment.Moment
17 | | Date;
18 |
19 | /** Parse string, moment, Date.
20 | *
21 | * Return unedfined on invalid input.
22 | */
23 | export function parseValue(value: ParseValueData, dateFormat: string, localization: string): moment.Moment {
24 | if (!isNil(value) && !isNil(dateFormat)) {
25 | const date = moment(value, dateFormat);
26 | if (date.isValid()) {
27 | date.locale(localization);
28 |
29 | return date;
30 | }
31 | }
32 | }
33 |
34 | type ParseArrayOrValueData =
35 | | ParseValueData
36 | | ParseValueData[];
37 |
38 | /** Parse string, moment, Date, string[], moment[], Date[].
39 | *
40 | * Return array of moments. Returned value contains only valid moments.
41 | * Return undefined if none of the input values are valid.
42 | */
43 | export function parseArrayOrValue(data: ParseArrayOrValueData, dateFormat: string, localization: string) {
44 | if (isArray(data)) {
45 | const parsed = compact((data as ParseValueData[]).map((item) => parseValue(item, dateFormat, localization)));
46 | if (parsed.length > 0) {
47 | return parsed;
48 | }
49 | }
50 | const parsedValue = parseValue((data as ParseValueData), dateFormat, localization);
51 |
52 | return parsedValue && [parsedValue];
53 | }
54 |
55 | interface DateParams {
56 | year?: number;
57 | month?: number;
58 | date?: number;
59 | hour?: number;
60 | minute?: number;
61 | }
62 |
63 | interface GetInitializerParams {
64 | initialDate?: ParseValueData;
65 | dateFormat?: string;
66 | dateParams?: DateParams;
67 | localization?: string;
68 | }
69 |
70 | /** Create moment.
71 | *
72 | * Creates moment using `dateParams` or `initialDate` arguments (if provided).
73 | * Precedense order: dateParams -> initialDate -> default value
74 | */
75 | export function getInitializer(context: GetInitializerParams): moment.Moment {
76 | const {
77 | dateParams,
78 | initialDate,
79 | dateFormat,
80 | localization,
81 | } = context;
82 | if (dateParams) {
83 | const parsedParams = localization ? moment(dateParams).locale(localization) : moment(dateParams);
84 | if (parsedParams.isValid()) {
85 | return parsedParams;
86 | }
87 | }
88 | const parsedInitialDate = parseValue(initialDate, dateFormat, localization);
89 | if (parsedInitialDate) {
90 | return parsedInitialDate;
91 | }
92 |
93 | return localization ? moment().locale(localization) : moment();
94 | }
95 |
96 | type InitialDate = string | moment.Moment | Date;
97 | type DateValue = InitialDate;
98 |
99 | /** Creates moment instance from provided value or initialDate.
100 | * Creates today by default.
101 | */
102 | export function buildValue(value: ParseValueData,
103 | initialDate: InitialDate,
104 | localization: string,
105 | dateFormat: string,
106 | defaultVal = moment()): Moment {
107 | const valueParsed = parseValue(value, dateFormat, localization);
108 | if (valueParsed) {
109 | return valueParsed;
110 | }
111 | const initialDateParsed = parseValue(initialDate, dateFormat, localization);
112 | if (initialDateParsed) {
113 | return initialDateParsed;
114 | }
115 | const _defaultVal = defaultVal ? defaultVal.clone() : defaultVal;
116 | if (_defaultVal) {
117 | _defaultVal.locale(localization);
118 | }
119 |
120 | return _defaultVal;
121 | }
122 |
123 | export function dateValueToString(value: DateValue, dateFormat: string, locale: string): string {
124 | if (isString(value)) {
125 | return value;
126 | }
127 | if (moment.isMoment(value)) {
128 | const _value = value.clone();
129 | _value.locale(locale);
130 |
131 | return _value.format(dateFormat);
132 | }
133 |
134 | const date = moment(value, dateFormat);
135 | if (date.isValid()) {
136 | date.locale(locale);
137 |
138 | return date.format(dateFormat);
139 | }
140 |
141 | return '';
142 | }
143 |
144 | function cleanDate(inputString: string, dateFormat: string): string {
145 | const formattedDateLength = moment().format(dateFormat).length;
146 |
147 | return inputString.trim().slice(0, formattedDateLength);
148 | }
149 |
150 | interface Range {
151 | start?: moment.Moment;
152 | end?: moment.Moment;
153 | }
154 |
155 | /**
156 | * Extract start and end dates from input string.
157 | * Return { start: Moment|undefined, end: Moment|undefined }
158 | * @param {string} inputString Row input string from user
159 | * @param {string} dateFormat Moment formatting string
160 | * @param {string} inputSeparator Separator for split inputString
161 | */
162 | export function parseDatesRange(
163 | inputString: string = '',
164 | dateFormat: string = '',
165 | inputSeparator: string = ' - ',
166 | ): Range {
167 | const dates = inputString.split(inputSeparator)
168 | .map((date) => cleanDate(date, dateFormat));
169 | const result: Range = {};
170 | let start;
171 | let end;
172 |
173 | start = moment(dates[0], dateFormat);
174 | if (dates.length === 2) {
175 | end = moment(dates[1], dateFormat);
176 | }
177 | if (start && start.isValid()) {
178 | result.start = start;
179 | }
180 | if (end && end.isValid()) {
181 | result.end = end;
182 | }
183 |
184 | return result;
185 | }
186 |
--------------------------------------------------------------------------------
/src/pickers/monthPicker/MonthPicker.tsx:
--------------------------------------------------------------------------------
1 | import filter from 'lodash/filter';
2 | import range from 'lodash/range';
3 | import includes from 'lodash/includes';
4 | import isNil from 'lodash/isNil';
5 |
6 | import * as React from 'react';
7 |
8 | import MonthView from '../../views/MonthView';
9 | import {
10 | BasePickerOnChangeData,
11 | BasePickerProps,
12 | DisableValuesProps,
13 | EnableValuesProps,
14 | MinMaxValueProps,
15 | OptionalHeaderProps,
16 | ProvideHeadingValue,
17 | SingleSelectionPicker,
18 | } from '../BasePicker';
19 | import {
20 | MONTH_PAGE_WIDTH,
21 | MONTHS_IN_YEAR,
22 | } from './const';
23 | import {
24 | buildCalendarValues,
25 | getDisabledPositions,
26 | getInitialDatePosition,
27 | isNextPageAvailable,
28 | isPrevPageAvailable,
29 | } from './sharedFunctions';
30 |
31 | type MonthPickerProps = BasePickerProps
32 | & DisableValuesProps
33 | & EnableValuesProps
34 | & MinMaxValueProps
35 | & OptionalHeaderProps;
36 |
37 | export interface MonthPickerOnChangeData extends BasePickerOnChangeData {
38 | value: {
39 | year: number,
40 | month: number,
41 | };
42 | }
43 |
44 | class MonthPicker
45 | extends SingleSelectionPicker
46 | implements ProvideHeadingValue {
47 | /*
48 | Note:
49 | use it like this
50 | to make react create new instance when input value changes
51 | */
52 | constructor(props) {
53 | super(props);
54 | this.PAGE_WIDTH = MONTH_PAGE_WIDTH;
55 | }
56 |
57 | public render() {
58 | const {
59 | onChange,
60 | value,
61 | initializeWith,
62 | closePopup,
63 | inline,
64 | isPickerInFocus,
65 | isTriggerInFocus,
66 | onCalendarViewMount,
67 | disable,
68 | enable,
69 | minDate,
70 | maxDate,
71 | localization,
72 | ...rest
73 | } = this.props;
74 |
75 | return (
76 |
93 | );
94 | }
95 |
96 | public getCurrentDate(): string {
97 | /* Return current year(string) to display in calendar header. */
98 | return this.state.date.year().toString();
99 | }
100 |
101 | protected buildCalendarValues(): string[] {
102 | const { localization } = this.props;
103 |
104 | return buildCalendarValues(localization);
105 | }
106 |
107 | protected getSelectableCellPositions(): number[] {
108 | return filter(
109 | range(0, MONTHS_IN_YEAR),
110 | (m) => !includes(this.getDisabledPositions(), m),
111 | );
112 | }
113 |
114 | protected getInitialDatePosition(): number {
115 | const selectable = this.getSelectableCellPositions();
116 |
117 | return getInitialDatePosition(selectable, this.state.date);
118 | }
119 |
120 | protected getActiveCellPosition(): number {
121 | /*
122 | Return position of a month that should be displayed as active
123 | (position in array returned by `this.buildCalendarValues`).
124 | */
125 | if (!isNil(this.props.value)) {
126 | if (this.props.value.year() === this.state.date.year()) {
127 | return this.props.value.month();
128 | }
129 | }
130 | }
131 |
132 | protected getDisabledPositions(): number[] {
133 | const {
134 | maxDate,
135 | minDate,
136 | enable,
137 | disable,
138 | } = this.props;
139 |
140 | return getDisabledPositions(enable, disable, maxDate, minDate, this.state.date);
141 | }
142 |
143 | protected isNextPageAvailable(): boolean {
144 | const {
145 | maxDate,
146 | enable,
147 | } = this.props;
148 |
149 | return isNextPageAvailable(maxDate, enable, this.state.date);
150 | }
151 |
152 | protected isPrevPageAvailable(): boolean {
153 | const {
154 | minDate,
155 | enable,
156 | } = this.props;
157 |
158 | return isPrevPageAvailable(minDate, enable, this.state.date);
159 | }
160 |
161 | protected handleChange = (e: React.SyntheticEvent, { value }): void => {
162 | const data: MonthPickerOnChangeData = {
163 | ...this.props,
164 | value: {
165 | year: parseInt(this.getCurrentDate(), 10),
166 | month: this.buildCalendarValues().indexOf(value),
167 | },
168 | };
169 | this.props.onChange(e, data);
170 | }
171 |
172 | protected switchToNextPage = (e: React.SyntheticEvent,
173 | data: any,
174 | callback: () => void): void => {
175 | this.setState(({ date }) => {
176 | const nextDate = date.clone();
177 | nextDate.add(1, 'year');
178 |
179 | return { date: nextDate };
180 | }, callback);
181 | }
182 |
183 | protected switchToPrevPage = (e: React.SyntheticEvent,
184 | data: any,
185 | callback: () => void): void => {
186 | this.setState(({ date }) => {
187 | const prevDate = date.clone();
188 | prevDate.subtract(1, 'year');
189 |
190 | return { date: prevDate };
191 | }, callback);
192 | }
193 | }
194 |
195 | export default MonthPicker;
196 |
--------------------------------------------------------------------------------
/src/pickers/timePicker/HourPicker.tsx:
--------------------------------------------------------------------------------
1 | import filter from 'lodash/filter';
2 | import range from 'lodash/range';
3 | import includes from 'lodash/includes';
4 | import isArray from 'lodash/isArray';
5 | import concat from 'lodash/concat';
6 | import uniq from 'lodash/uniq';
7 | import sortBy from 'lodash/sortBy';
8 |
9 | import * as React from 'react';
10 |
11 | import HourView from '../../views/HourView';
12 | import {
13 | BasePickerOnChangeData,
14 | BasePickerProps,
15 | DisableValuesProps,
16 | MinMaxValueProps,
17 | OptionalHeaderProps,
18 | ProvideHeadingValue,
19 | SingleSelectionPicker,
20 | TimeFormat,
21 | TimePickerProps,
22 | } from '../BasePicker';
23 | import {
24 | buildTimeStringWithSuffix,
25 | getCurrentDate,
26 | isNextPageAvailable,
27 | isPrevPageAvailable,
28 | } from './sharedFunctions';
29 |
30 | const HOURS_ON_PAGE = 24;
31 | const PAGE_WIDTH = 4;
32 |
33 | type HourPickerProps = BasePickerProps
34 | & MinMaxValueProps
35 | & DisableValuesProps
36 | & TimePickerProps
37 | & OptionalHeaderProps;
38 |
39 | export interface HourPickerOnChangeData extends BasePickerOnChangeData {
40 | value: {
41 | year: number,
42 | month: number,
43 | date: number,
44 | hour: number,
45 | };
46 | }
47 |
48 | class HourPicker
49 | extends SingleSelectionPicker
50 | implements ProvideHeadingValue {
51 | public static readonly defaultProps: { timeFormat: TimeFormat } = {
52 | timeFormat: '24',
53 | };
54 |
55 | constructor(props) {
56 | super(props);
57 | this.PAGE_WIDTH = PAGE_WIDTH;
58 | }
59 |
60 | public render() {
61 | const {
62 | onChange,
63 | value,
64 | initializeWith,
65 | closePopup,
66 | inline,
67 | isPickerInFocus,
68 | isTriggerInFocus,
69 | onCalendarViewMount,
70 | minDate,
71 | maxDate,
72 | disable,
73 | timeFormat,
74 | localization,
75 | ...rest
76 | } = this.props;
77 |
78 | return (
79 |
96 | );
97 | }
98 |
99 | public getCurrentDate(): string {
100 | /* Return currently selected month, date and year(string) to display in calendar header. */
101 | return getCurrentDate(this.state.date);
102 | }
103 |
104 | protected buildCalendarValues(): string[] {
105 | /*
106 | Return array of hours (strings) like ['16:00', '17:00', ...]
107 | that used to populate calendar's page.
108 | */
109 | return range(0, 24).map((h) => {
110 | return `${h < 10 ? '0' : ''}${h}`;
111 | }).map((hour) => buildTimeStringWithSuffix(hour, '00', this.props.timeFormat));
112 | }
113 |
114 | protected getSelectableCellPositions(): number[] {
115 | return filter(
116 | range(0, HOURS_ON_PAGE),
117 | (h) => !includes(this.getDisabledPositions(), h),
118 | );
119 | }
120 |
121 | protected getInitialDatePosition(): number {
122 | const selectable = this.getSelectableCellPositions();
123 | if (selectable.indexOf(this.state.date.hour()) < 0) {
124 | return selectable[0];
125 | }
126 |
127 | return this.state.date.hour();
128 | }
129 |
130 | protected getActiveCellPosition(): number {
131 | /*
132 | Return position of an hour that should be displayed as active
133 | (position in array returned by `this.buildCalendarValues`).
134 | */
135 | const { value } = this.props;
136 | if (value && value.isSame(this.state.date, 'date')) {
137 | return this.props.value.hour();
138 | }
139 | }
140 |
141 | protected isNextPageAvailable(): boolean {
142 | return isNextPageAvailable(this.state.date, this.props.maxDate);
143 | }
144 |
145 | protected isPrevPageAvailable(): boolean {
146 | return isPrevPageAvailable(this.state.date, this.props.minDate);
147 | }
148 |
149 | protected getDisabledPositions(): number[] {
150 | /*
151 | Return position numbers of hours that should be displayed as disabled
152 | (position in array returned by `this.buildCalendarValues`).
153 | */
154 | const {
155 | disable,
156 | minDate,
157 | maxDate,
158 | } = this.props;
159 | let disabledByDisable = [];
160 | let disabledByMaxDate = [];
161 | let disabledByMinDate = [];
162 |
163 | if (isArray(disable)) {
164 | disabledByDisable = concat(
165 | disabledByDisable,
166 | disable.filter((date) => date.isSame(this.state.date, 'day'))
167 | .map((date) => date.hour()));
168 | }
169 | if (minDate) {
170 | if (minDate.isSame(this.state.date, 'day')) {
171 | disabledByMinDate = concat(
172 | disabledByMinDate,
173 | range(0 , minDate.hour()));
174 | }
175 | }
176 | if (maxDate) {
177 | if (maxDate.isSame(this.state.date, 'day')) {
178 | disabledByMaxDate = concat(
179 | disabledByMaxDate,
180 | range(maxDate.hour() + 1, 24));
181 | }
182 | }
183 | const result = sortBy(
184 | uniq(
185 | concat(disabledByDisable, disabledByMaxDate, disabledByMinDate)));
186 | if (result.length > 0) {
187 | return result;
188 | }
189 | }
190 |
191 | protected handleChange = (e: React.SyntheticEvent, { value }): void => {
192 | const data: HourPickerOnChangeData = {
193 | ...this.props,
194 | value: {
195 | year: this.state.date.year(),
196 | month: this.state.date.month(),
197 | date: this.state.date.date(),
198 | hour: this.buildCalendarValues().indexOf(value),
199 | },
200 | };
201 | this.props.onChange(e, data);
202 | }
203 |
204 | protected switchToNextPage = (e: React.SyntheticEvent,
205 | data: any,
206 | callback: () => void): void => {
207 | this.setState(({ date }) => {
208 | const nextDate = date.clone();
209 | nextDate.add(1, 'day');
210 |
211 | return { date: nextDate };
212 | }, callback);
213 | }
214 |
215 | protected switchToPrevPage = (e: React.SyntheticEvent,
216 | data: any,
217 | callback: () => void): void => {
218 | this.setState(({ date }) => {
219 | const prevDate = date.clone();
220 | prevDate.subtract(1, 'day');
221 |
222 | return { date: prevDate };
223 | }, callback);
224 | }
225 | }
226 |
227 | export default HourPicker;
228 |
--------------------------------------------------------------------------------
/src/pickers/dayPicker/DayPicker.tsx:
--------------------------------------------------------------------------------
1 | import filter from 'lodash/filter';
2 | import range from 'lodash/range';
3 | import includes from 'lodash/includes';
4 | import isArray from 'lodash/isArray';
5 | import some from 'lodash/some';
6 |
7 | import * as React from 'react';
8 |
9 | import DayView from '../../views/DayView';
10 | import { WEEKS_TO_DISPLAY } from '../../views/DayView';
11 | import {
12 | BasePickerOnChangeData,
13 | BasePickerProps,
14 | DisableValuesProps,
15 | EnableValuesProps,
16 | MinMaxValueProps,
17 | MarkedValuesProps,
18 | ProvideHeadingValue,
19 | SingleSelectionPicker,
20 | } from '../BasePicker';
21 | import {
22 | buildDays,
23 | getDisabledDays,
24 | getMarkedDays,
25 | getInitialDatePosition,
26 | isNextPageAvailable,
27 | isPrevPageAvailable,
28 | } from './sharedFunctions';
29 |
30 | const PAGE_WIDTH = 7;
31 | export const DAYS_ON_PAGE = WEEKS_TO_DISPLAY * PAGE_WIDTH;
32 |
33 | export interface DayPickerOnChangeData extends BasePickerOnChangeData {
34 | value: {
35 | year: number;
36 | month: number;
37 | date: number;
38 | };
39 | }
40 |
41 | type DayPickerProps = BasePickerProps
42 | & DisableValuesProps
43 | & EnableValuesProps
44 | & MinMaxValueProps
45 | & MarkedValuesProps;
46 |
47 | class DayPicker
48 | extends SingleSelectionPicker
49 | implements ProvideHeadingValue {
50 | constructor(props) {
51 | super(props);
52 | this.PAGE_WIDTH = PAGE_WIDTH;
53 | }
54 |
55 | public render() {
56 | const {
57 | onChange,
58 | value,
59 | initializeWith,
60 | closePopup,
61 | inline,
62 | isPickerInFocus,
63 | isTriggerInFocus,
64 | onCalendarViewMount,
65 | disable,
66 | enable,
67 | minDate,
68 | maxDate,
69 | marked,
70 | markColor,
71 | localization,
72 | ...rest
73 | } = this.props;
74 |
75 | return (
76 |
95 | );
96 | }
97 |
98 | public getCurrentDate(): string {
99 | /* Return currently selected year and month(string) to display in calendar header. */
100 | return this.state.date.format('MMMM YYYY');
101 | }
102 |
103 | protected buildCalendarValues(): string[] {
104 | /*
105 | Return array of dates (strings) like ['31', '1', ...]
106 | that used to populate calendar's page.
107 | */
108 | return buildDays(this.state.date, DAYS_ON_PAGE);
109 | }
110 |
111 | protected getSelectableCellPositions(): number[] {
112 | return filter(
113 | range(0, DAYS_ON_PAGE),
114 | (d) => !includes(this.getDisabledPositions(), d),
115 | );
116 | }
117 |
118 | protected getInitialDatePosition(): number {
119 | return getInitialDatePosition(this.state.date.date().toString(),
120 | this.buildCalendarValues(),
121 | this.getSelectableCellPositions());
122 | }
123 |
124 | protected getActiveCellPosition(): number {
125 | /*
126 | Return position of a date that should be displayed as active
127 | (position in array returned by `this.buildCalendarValues`).
128 | */
129 | if (this.props.value && this.props.value.isSame(this.state.date, 'month')) {
130 | const disabledPositions = this.getDisabledPositions();
131 | const active = this.buildCalendarValues()
132 | .map((day, i) => includes(disabledPositions, i) ? undefined : day)
133 | .indexOf(this.props.value.date().toString());
134 | if (active >= 0) {
135 | return active;
136 | }
137 | }
138 | }
139 |
140 | protected getDisabledPositions(): number[] {
141 | /*
142 | Return position numbers of dates that should be displayed as disabled
143 | (position in array returned by `this.buildCalendarValues`).
144 | */
145 | const {
146 | disable,
147 | maxDate,
148 | minDate,
149 | enable,
150 | } = this.props;
151 |
152 | return getDisabledDays(disable, maxDate, minDate, this.state.date, DAYS_ON_PAGE, enable);
153 | }
154 |
155 | protected getMarkedPositions(): number[] {
156 | /*
157 | Return position numbers of dates that should be displayed as marked
158 | (position in array returned by `this.buildCalendarValues`).
159 | */
160 | const {
161 | marked,
162 | } = this.props;
163 |
164 | if (marked) {
165 | return getMarkedDays(marked, this.state.date, DAYS_ON_PAGE);
166 | } else {
167 | return [];
168 | }
169 | }
170 |
171 | protected isNextPageAvailable = (): boolean => {
172 | const {
173 | maxDate,
174 | enable,
175 | } = this.props;
176 | if (isArray(enable)) {
177 | return some(enable, (enabledDate) => enabledDate.isAfter(this.state.date, 'month'));
178 | }
179 |
180 | return isNextPageAvailable(this.state.date, maxDate);
181 | }
182 |
183 | protected isPrevPageAvailable = (): boolean => {
184 | const {
185 | minDate,
186 | enable,
187 | } = this.props;
188 | if (isArray(enable)) {
189 | return some(enable, (enabledDate) => enabledDate.isBefore(this.state.date, 'month'));
190 | }
191 |
192 | return isPrevPageAvailable(this.state.date, minDate);
193 | }
194 |
195 | protected handleChange = (e: React.SyntheticEvent, { value }): void => {
196 | // `value` is selected date(string) like '31' or '1'
197 | const data: DayPickerOnChangeData = {
198 | ...this.props,
199 | value: {
200 | year: this.state.date.year(),
201 | month: this.state.date.month(),
202 | date: parseInt(value, 10),
203 | },
204 | };
205 |
206 | this.props.onChange(e, data);
207 | }
208 |
209 | protected switchToNextPage = (e: React.SyntheticEvent,
210 | data: any,
211 | callback: () => void): void => {
212 | this.setState(({ date }) => {
213 | const nextDate = date.clone();
214 | nextDate.add(1, 'month');
215 |
216 | return { date: nextDate };
217 | }, callback);
218 | }
219 |
220 | protected switchToPrevPage = (e: React.SyntheticEvent,
221 | data: any,
222 | callback: () => void): void => {
223 | this.setState(({ date }) => {
224 | const prevDate = date.clone();
225 | prevDate.subtract(1, 'month');
226 |
227 | return { date: prevDate };
228 | }, callback);
229 | }
230 | }
231 |
232 | export default DayPicker;
233 |
--------------------------------------------------------------------------------
/src/pickers/monthPicker/MonthRangePicker.tsx:
--------------------------------------------------------------------------------
1 | import filter from 'lodash/filter';
2 | import range from 'lodash/range';
3 | import includes from 'lodash/includes';
4 | import isNil from 'lodash/isNil';
5 |
6 | import {Moment} from 'moment';
7 | import moment from 'moment';
8 | import * as React from 'react';
9 |
10 | import {RangeIndexes} from '../../views/BaseCalendarView';
11 | import MonthRangeView from '../../views/MonthRangeView';
12 | import {
13 | BasePickerOnChangeData,
14 | BasePickerProps,
15 | MinMaxValueProps,
16 | ProvideHeadingValue,
17 | RangeSelectionPicker,
18 | } from '../BasePicker';
19 | import {
20 | MONTH_PAGE_WIDTH,
21 | MONTHS_IN_YEAR,
22 | } from './const';
23 | import {
24 | buildCalendarValues,
25 | getDisabledPositions,
26 | getInitialDatePosition,
27 | isNextPageAvailable,
28 | isPrevPageAvailable,
29 | } from './sharedFunctions';
30 |
31 | interface MonthRangePickerProps extends BasePickerProps, MinMaxValueProps {
32 | /** Moment date formatting string. */
33 | dateFormat: string;
34 | /** Start of currently selected dates range. */
35 | start: Moment;
36 | /** End of currently selected dates range. */
37 | end: Moment;
38 | }
39 |
40 | export type MonthRangePickerOnChangeData = BasePickerOnChangeData;
41 |
42 | class MonthRangePicker
43 | extends RangeSelectionPicker
44 | implements ProvideHeadingValue {
45 | constructor(props) {
46 | super(props);
47 | this.PAGE_WIDTH = MONTH_PAGE_WIDTH;
48 | }
49 |
50 | public render() {
51 | const {
52 | onChange,
53 | initializeWith,
54 | closePopup,
55 | inline,
56 | isPickerInFocus,
57 | isTriggerInFocus,
58 | onCalendarViewMount,
59 | dateFormat,
60 | start,
61 | end,
62 | minDate,
63 | maxDate,
64 | localization,
65 | ...rest
66 | } = this.props;
67 |
68 | return (
69 |
87 | );
88 | }
89 |
90 | public getCurrentDate(): string {
91 | /* Return currently selected year and month(string) to display in calendar header. */
92 | return this.state.date.format('YYYY');
93 | }
94 |
95 | protected buildCalendarValues(): string[] {
96 | const { localization } = this.props;
97 |
98 | return buildCalendarValues(localization);
99 | }
100 |
101 | protected getSelectableCellPositions(): number[] {
102 | return filter(
103 | range(0, MONTHS_IN_YEAR),
104 | (d) => !includes(this.getDisabledPositions(), d),
105 | );
106 | }
107 |
108 | protected getActiveCellsPositions(): RangeIndexes {
109 | /*
110 | Return starting and ending positions of month range that should be displayed as active
111 | { start: number, end: number }
112 | */
113 | const {
114 | start,
115 | end,
116 | } = this.props;
117 | const currentYear = this.state.date.year();
118 | const result = {
119 | start: undefined,
120 | end: undefined,
121 | };
122 |
123 | if (start && end) {
124 | if (currentYear < start.year() || currentYear > end.year()) {
125 | return result;
126 | }
127 |
128 | result.start = currentYear === start.year() ? start.month() : 0;
129 | result.end = currentYear === end.year() ? end.month() : MONTHS_IN_YEAR - 1;
130 | }
131 | if (start && !end) {
132 | result.start = currentYear === start.year() ? start.month() : undefined;
133 | }
134 |
135 | return result;
136 | }
137 |
138 | protected getDisabledPositions(): number[] {
139 | /*
140 | Return position numbers of dates that should be displayed as disabled
141 | (position in array returned by `this.buildCalendarValues`).
142 | */
143 | const {
144 | maxDate,
145 | minDate,
146 | } = this.props;
147 |
148 | return getDisabledPositions(undefined, undefined, maxDate, minDate, this.state.date);
149 | }
150 |
151 | protected isNextPageAvailable(): boolean {
152 | const {maxDate} = this.props;
153 |
154 | return isNextPageAvailable(maxDate, undefined, this.state.date);
155 | }
156 |
157 | protected isPrevPageAvailable(): boolean {
158 | const {minDate} = this.props;
159 |
160 | return isPrevPageAvailable(minDate, undefined, this.state.date);
161 | }
162 |
163 | protected getSelectedRange(): string {
164 | /* Return currently selected dates range(string) to display in calendar header. */
165 | const {
166 | start,
167 | end,
168 | dateFormat,
169 | } = this.props;
170 |
171 | return `${start ? start.format(dateFormat) : '- - -'} - ${end ? end.format(dateFormat) : '- - -'}`;
172 | }
173 |
174 | protected handleChange = (e: React.SyntheticEvent, {itemPosition}) => {
175 | // call `onChange` with value: { start: moment, end: moment }
176 | const {
177 | start,
178 | end,
179 | localization,
180 | } = this.props;
181 | const data: MonthRangePickerOnChangeData = {
182 | ...this.props,
183 | value: {},
184 | };
185 |
186 | if (isNil(start) && isNil(end)) {
187 | data.value =
188 | localization
189 | ? {start: moment({year: this.state.date.year(), month: itemPosition, date: 1}).locale(localization)}
190 | : {start: moment({year: this.state.date.year(), month: itemPosition, date: 1})};
191 | } else if (!isNil(start) && isNil(end)) {
192 | data.value =
193 | localization
194 | ? {
195 | start,
196 | end: moment({year: this.state.date.year(), month: itemPosition, date: 1}).locale(localization).endOf('month'),
197 | }
198 | : {
199 | start,
200 | end: moment({year: this.state.date.year(), month: itemPosition, date: 1}).endOf('month'),
201 | };
202 | }
203 |
204 | this.props.onChange(e, data);
205 | }
206 |
207 | protected switchToNextPage = (e: React.SyntheticEvent,
208 | data: any,
209 | callback: () => void): void => {
210 | this.setState(({date}) => {
211 | const nextDate = date.clone();
212 | nextDate.add(1, 'year');
213 |
214 | return {date: nextDate};
215 | }, callback);
216 | }
217 |
218 | protected switchToPrevPage = (e: React.SyntheticEvent,
219 | data: any,
220 | callback: () => void): void => {
221 | this.setState(({date}) => {
222 | const prevDate = date.clone();
223 | prevDate.subtract(1, 'year');
224 |
225 | return {date: prevDate};
226 | }, callback);
227 | }
228 |
229 | protected getInitialDatePosition = (): number => {
230 | const selectable = this.getSelectableCellPositions();
231 |
232 | return getInitialDatePosition(selectable, this.state.date);
233 | }
234 | }
235 |
236 | export default MonthRangePicker;
237 |
--------------------------------------------------------------------------------
/src/pickers/timePicker/MinutePicker.tsx:
--------------------------------------------------------------------------------
1 | import range from 'lodash/range';
2 | import isArray from 'lodash/isArray';
3 | import concat from 'lodash/concat';
4 | import uniq from 'lodash/uniq';
5 | import sortBy from 'lodash/sortBy';
6 |
7 | import * as React from 'react';
8 |
9 | import MinuteView from '../../views/MinuteView';
10 | import {
11 | BasePickerOnChangeData,
12 | BasePickerProps,
13 | DisableValuesProps,
14 | MinMaxValueProps,
15 | OptionalHeaderProps,
16 | ProvideHeadingValue,
17 | SingleSelectionPicker,
18 | TimeFormat,
19 | TimePickerProps,
20 | } from '../BasePicker';
21 | import {
22 | buildTimeStringWithSuffix,
23 | getCurrentDate,
24 | isNextPageAvailable,
25 | isPrevPageAvailable,
26 | } from './sharedFunctions';
27 |
28 | const MINUTES_STEP = 5;
29 | const MINUTES_ON_PAGE = 12;
30 | const PAGE_WIDTH = 3;
31 |
32 | type MinutePickerProps = BasePickerProps
33 | & MinMaxValueProps
34 | & DisableValuesProps
35 | & TimePickerProps
36 | & OptionalHeaderProps;
37 |
38 | export interface MinutePickerOnChangeData extends BasePickerOnChangeData {
39 | value: {
40 | year: number,
41 | month: number,
42 | date: number,
43 | hour: number,
44 | minute: number,
45 | };
46 | }
47 |
48 | class MinutePicker
49 | extends SingleSelectionPicker
50 | implements ProvideHeadingValue {
51 | public static readonly defaultProps: { timeFormat: TimeFormat } = {
52 | timeFormat: '24',
53 | };
54 |
55 | constructor(props) {
56 | super(props);
57 | this.PAGE_WIDTH = PAGE_WIDTH;
58 | }
59 |
60 | public render() {
61 | const {
62 | onChange,
63 | value,
64 | initializeWith,
65 | closePopup,
66 | inline,
67 | isPickerInFocus,
68 | isTriggerInFocus,
69 | onCalendarViewMount,
70 | minDate,
71 | maxDate,
72 | disable,
73 | timeFormat,
74 | localization,
75 | ...rest
76 | } = this.props;
77 |
78 | return (
79 |
96 | );
97 | }
98 |
99 | public getCurrentDate(): string {
100 | /* Return currently selected month, date and year(string) to display in calendar header. */
101 | return getCurrentDate(this.state.date);
102 | }
103 |
104 | protected buildCalendarValues(): string[] {
105 | /*
106 | Return array of minutes (strings) like ['16:15', '16:20', ...]
107 | that used to populate calendar's page.
108 | */
109 | const hour = this.state.date.hour() < 10
110 | ? '0' + this.state.date.hour().toString()
111 | : this.state.date.hour().toString();
112 |
113 | return range(0, 60, MINUTES_STEP)
114 | .map((minute) => `${minute < 10 ? '0' : ''}${minute}`)
115 | .map((minute) => buildTimeStringWithSuffix(hour, minute, this.props.timeFormat));
116 | }
117 |
118 | protected getSelectableCellPositions(): number[] {
119 | const disabled = this.getDisabledPositions();
120 | const all = range(0, MINUTES_ON_PAGE);
121 | if (disabled) {
122 | return all.filter((pos) => {
123 | return disabled.indexOf(pos) < 0;
124 | });
125 | }
126 |
127 | return all;
128 | }
129 |
130 | protected getInitialDatePosition(): number {
131 | const selectable = this.getSelectableCellPositions();
132 | if (selectable.indexOf(getMinuteCellPosition(this.state.date.minute())) < 0) {
133 | return selectable[0];
134 | }
135 |
136 | return getMinuteCellPosition(this.state.date.minute());
137 | }
138 |
139 | protected getDisabledPositions(): number[] {
140 | const {
141 | disable,
142 | minDate,
143 | maxDate,
144 | } = this.props;
145 | let disabledByDisable = [];
146 | let disabledByMaxDate = [];
147 | let disabledByMinDate = [];
148 |
149 | if (isArray(disable)) {
150 | disabledByDisable = concat(
151 | disabledByDisable,
152 | disable.filter((date) => date.isSame(this.state.date, 'day'))
153 | .map((date) => getMinuteCellPosition(date.minute())));
154 | }
155 | if (minDate) {
156 | if (minDate.isSame(this.state.date, 'hour')) {
157 | disabledByMinDate = concat(
158 | disabledByMinDate,
159 | range(0 , minDate.minute()).map((m) => getMinuteCellPosition(m)));
160 | }
161 | }
162 | if (maxDate) {
163 | if (maxDate.isSame(this.state.date, 'hour')) {
164 | disabledByMaxDate = concat(
165 | disabledByMaxDate,
166 | range(maxDate.minute() + MINUTES_STEP, 60).map((m) => getMinuteCellPosition(m)));
167 | }
168 | }
169 | const result = sortBy(
170 | uniq(
171 | concat(disabledByDisable, disabledByMaxDate, disabledByMinDate)));
172 | if (result.length > 0) {
173 | return result;
174 | }
175 | }
176 |
177 | protected getActiveCellPosition(): number {
178 | /*
179 | Return position of a minute that should be displayed as active
180 | (position in array returned by `this.buildCalendarValues`).
181 | */
182 | const { value } = this.props;
183 | if (value && value.isSame(this.state.date, 'date')) {
184 | return Math.floor(this.props.value.minutes() / MINUTES_STEP);
185 | }
186 | }
187 |
188 | protected isNextPageAvailable(): boolean {
189 | return isNextPageAvailable(this.state.date, this.props.maxDate);
190 | }
191 |
192 | protected isPrevPageAvailable(): boolean {
193 | return isPrevPageAvailable(this.state.date, this.props.minDate);
194 | }
195 |
196 | protected handleChange = (e: React.SyntheticEvent, { value }): void => {
197 | const data: MinutePickerOnChangeData = {
198 | ...this.props,
199 | value: {
200 | year: this.state.date.year(),
201 | month: this.state.date.month(),
202 | date: this.state.date.date(),
203 | hour: this.state.date.hour(),
204 | minute: this.buildCalendarValues().indexOf(value) * MINUTES_STEP,
205 | },
206 | };
207 | this.props.onChange(e, data);
208 | }
209 |
210 | protected switchToNextPage = (e: React.SyntheticEvent,
211 | data: any,
212 | callback: () => void): void => {
213 | this.setState(({ date }) => {
214 | const nextDate = date.clone();
215 | nextDate.add(1, 'day');
216 |
217 | return { date: nextDate };
218 | }, callback);
219 | }
220 |
221 | protected switchToPrevPage = (e: React.SyntheticEvent,
222 | data: any,
223 | callback: () => void): void => {
224 | this.setState(({ date }) => {
225 | const prevDate = date.clone();
226 | prevDate.subtract(1, 'day');
227 |
228 | return { date: prevDate };
229 | }, callback);
230 | }
231 | }
232 |
233 | function getMinuteCellPosition(minute: number): number {
234 | return Math.floor(minute / MINUTES_STEP);
235 | }
236 |
237 | export default MinutePicker;
238 |
--------------------------------------------------------------------------------
/src/pickers/dayPicker/sharedFunctions.ts:
--------------------------------------------------------------------------------
1 | import indexOf from 'lodash/indexOf';
2 | import lastIndexOf from 'lodash/lastIndexOf';
3 | import range from 'lodash/range';
4 | import includes from 'lodash/includes';
5 | import isNil from 'lodash/isNil';
6 | import isArray from 'lodash/isArray';
7 | import concat from 'lodash/concat';
8 | import uniq from 'lodash/uniq';
9 | import first from 'lodash/first';
10 | import sortBy from 'lodash/sortBy';
11 | import slice from 'lodash/slice';
12 | import find from 'lodash/find';
13 |
14 | import { Moment } from 'moment';
15 |
16 | /** Build days to fill page. */
17 | export function buildDays(date: Moment, daysOnPage: number) {
18 | const start = date.clone().startOf('month').startOf('week');
19 |
20 | return getDaysArray(
21 | start.date(),
22 | getBrakepoints(date),
23 | daysOnPage).map((d) => d.toString());
24 | }
25 |
26 | /** Return dates from ends of months.
27 | *
28 | * On one datepicker's page not only days from current month are displayed
29 | * but also some days from adjacent months. This function returns days
30 | * that separate one month from other (last day in month).
31 | * Return array of one or two numbers.
32 | */
33 | function getBrakepoints(referenceDate: Moment): number[] {
34 | const dateInCurrentMonth = referenceDate.clone();
35 | const currentMonth = dateInCurrentMonth.month();
36 | const brakepoints = [];
37 |
38 | const firstDateOnPage = dateInCurrentMonth.clone().startOf('month').startOf('week');
39 | if (firstDateOnPage.month() !== currentMonth) {
40 | brakepoints.push(firstDateOnPage.clone().endOf('month').date());
41 | }
42 | brakepoints.push(dateInCurrentMonth.clone().endOf('month').date());
43 |
44 | return brakepoints;
45 | }
46 |
47 | /* Return array of day positions that are not disabled by default. */
48 | export function getDefaultEnabledDayPositions(allDays: string[], date: Moment): number[] {
49 | const dateClone = date.clone();
50 | const brakepoints = getBrakepoints(dateClone);
51 | if (brakepoints.length === 1) {
52 | return range(0, indexOf(allDays, brakepoints[0].toString()) + 1);
53 | } else {
54 | return range(indexOf(allDays, brakepoints[0].toString()) + 1,
55 | lastIndexOf(allDays, brakepoints[1].toString()) + 1);
56 | }
57 | }
58 |
59 | /** Return day positions that shoud be displayed as disabled. */
60 | export function getDisabledDays(
61 | disable: Moment[],
62 | maxDate: Moment,
63 | minDate: Moment,
64 | currentDate: Moment,
65 | daysOnPage: number,
66 | enable: Moment[]): number[] {
67 | const dayPositions = range(daysOnPage);
68 | const daysInCurrentMonthPositions = getDefaultEnabledDayPositions(buildDays(currentDate, daysOnPage), currentDate);
69 | let disabledDays = dayPositions.filter((dayPosition) => !includes(daysInCurrentMonthPositions, dayPosition));
70 | if (isArray(enable)) {
71 | const enabledDaysPositions = enable
72 | .filter((date) => date.isSame(currentDate, 'month'))
73 | .map((date) => date.date())
74 | .map((date) => daysInCurrentMonthPositions[date - 1]);
75 | disabledDays = concat(disabledDays,
76 | dayPositions.filter((position) => {
77 | return !includes(enabledDaysPositions, position);
78 | }));
79 | }
80 | if (isArray(disable)) {
81 | disabledDays = concat(disabledDays,
82 | disable
83 | .filter((date) => date.isSame(currentDate, 'month'))
84 | .map((date) => date.date())
85 | .map((date) => daysInCurrentMonthPositions[date - 1]));
86 | }
87 | if (!isNil(maxDate)) {
88 | if (maxDate.isBefore(currentDate, 'month')) {
89 | disabledDays = dayPositions;
90 | }
91 | if (maxDate.isSame(currentDate, 'month')) {
92 | disabledDays = concat(disabledDays,
93 | range(1, daysInCurrentMonthPositions.length + 1)
94 | .filter((date) => date > maxDate.date())
95 | .map((date) => daysInCurrentMonthPositions[date - 1]));
96 | }
97 | }
98 | if (!isNil(minDate)) {
99 | if (minDate.isAfter(currentDate, 'month')) {
100 | disabledDays = dayPositions;
101 | }
102 | if (minDate.isSame(currentDate, 'month')) {
103 | disabledDays = concat(disabledDays,
104 | range(1, daysInCurrentMonthPositions.length + 1)
105 | .filter((date) => date < minDate.date())
106 | .map((date) => daysInCurrentMonthPositions[date - 1]));
107 | }
108 | }
109 |
110 | return sortBy(uniq(disabledDays).filter((day) => !isNil(day)));
111 | }
112 |
113 | /** Return day positions that should be displayed as marked. */
114 | export function getMarkedDays(
115 | marked: Moment[],
116 | currentDate: Moment,
117 | daysOnPage: number): number[] {
118 | if (marked.length === 0) {
119 | return [];
120 | }
121 | const allDates = buildDays(currentDate, daysOnPage);
122 | const activeDayPositions = getDefaultEnabledDayPositions(allDates, currentDate);
123 | const allDatesNumb = allDates.map((date) => parseInt(date, 10));
124 |
125 | /*
126 | * The following will clear all dates before the 1st of the current month.
127 | * This is to prevent marking days before the 1st, that shouldn't be marked.
128 | * If the incorrect dates are marked, instead of the legitimate ones, the legitimate dates
129 | * will not be marked at all.
130 | */
131 | const fillTo = allDatesNumb.indexOf(1);
132 | for (let i = 0; i < fillTo; i++) {
133 | allDatesNumb[i] = 0;
134 | }
135 |
136 | const markedIndexes = marked
137 | .filter((date) => date.isSame(currentDate, 'month'))
138 | .map((date) => date.date())
139 | .map((date) => allDatesNumb.indexOf(date));
140 |
141 | return markedIndexes.filter((index) => includes(activeDayPositions, index));
142 | }
143 |
144 | export function isNextPageAvailable(date: Moment, maxDate: Moment): boolean {
145 | if (isNil(maxDate)) {
146 | return true;
147 | }
148 | if (date.isSameOrAfter(maxDate, 'month')) {
149 | return false;
150 | }
151 |
152 | return true;
153 | }
154 |
155 | export function isPrevPageAvailable(date: Moment, minDate: Moment): boolean {
156 | if (isNil(minDate)) {
157 | return true;
158 | }
159 | if (date.isSameOrBefore(minDate, 'month')) {
160 | return false;
161 | }
162 |
163 | return true;
164 | }
165 |
166 | // helper
167 | function getDaysArray(start: number, brakepoints: number[], length: number): number[] {
168 | let currentDay = start;
169 | const days = [];
170 | let brakepointsLeft = brakepoints.slice();
171 |
172 | while (! (days.length === length)) {
173 | days.push(currentDay);
174 | const bp = first(brakepointsLeft);
175 | if (currentDay === bp) {
176 | currentDay = 1;
177 | brakepointsLeft = slice(brakepointsLeft, 1);
178 | } else {
179 | currentDay = currentDay + 1;
180 | }
181 | }
182 |
183 | return days;
184 | }
185 |
186 | export const testExport = {
187 | buildDays,
188 | getBrakepoints,
189 | getDisabledDays,
190 | isNextPageAvailable,
191 | isPrevPageAvailable,
192 | getDaysArray,
193 | getDefaultEnabledDayPositions,
194 | };
195 |
196 | export function getInitialDatePosition(initDate: string,
197 | values: string[],
198 | selectablePositions: number[]): number {
199 | const selectable = selectablePositions.reduce((acc, pos) => {
200 | acc.push({ value: values[pos], position: pos });
201 |
202 | return acc;
203 | }, []);
204 | const res = find(selectable, (item) => item.value === initDate);
205 | if (res) {
206 | return res.position;
207 | }
208 |
209 | return selectable[0].position;
210 | }
211 |
--------------------------------------------------------------------------------
/src/pickers/YearPicker.tsx:
--------------------------------------------------------------------------------
1 | import range from 'lodash/range';
2 | import includes from 'lodash/includes';
3 | import isNil from 'lodash/isNil';
4 | import isArray from 'lodash/isArray';
5 | import concat from 'lodash/concat';
6 | import uniq from 'lodash/uniq';
7 | import filter from 'lodash/filter';
8 | import last from 'lodash/last';
9 | import first from 'lodash/first';
10 | import some from 'lodash/some';
11 |
12 | import * as React from 'react';
13 |
14 | import YearView from '../views/YearView';
15 | import {
16 | BasePickerOnChangeData,
17 | BasePickerProps,
18 | DisableValuesProps,
19 | EnableValuesProps,
20 | MinMaxValueProps,
21 | SingleSelectionPicker,
22 | } from './BasePicker';
23 |
24 | const PAGE_WIDTH = 3;
25 | const PAGE_HEIGHT = 4;
26 | const YEARS_ON_PAGE = PAGE_WIDTH * PAGE_HEIGHT;
27 |
28 | type YearPickerProps = BasePickerProps
29 | & DisableValuesProps
30 | & EnableValuesProps
31 | & MinMaxValueProps;
32 |
33 | export interface YearPickerOnChangeData extends BasePickerOnChangeData {
34 | value: {
35 | year: number,
36 | };
37 | }
38 |
39 | class YearPicker extends SingleSelectionPicker {
40 | /*
41 | Note:
42 | use it like this
43 | to make react create new instance when input value changes
44 | */
45 | constructor(props) {
46 | super(props);
47 | this.PAGE_WIDTH = PAGE_WIDTH;
48 | }
49 |
50 | public render() {
51 | const {
52 | onChange,
53 | value,
54 | initializeWith,
55 | closePopup,
56 | inline,
57 | isPickerInFocus,
58 | isTriggerInFocus,
59 | onCalendarViewMount,
60 | disable,
61 | enable,
62 | minDate,
63 | maxDate,
64 | localization,
65 | ...rest
66 | } = this.props;
67 |
68 | return (
69 |
85 | );
86 | }
87 |
88 | protected buildCalendarValues(): string[] {
89 | /*
90 | Return array of years (strings) like ['2012', '2013', ...]
91 | that used to populate calendar's page.
92 | */
93 | const years = [];
94 | const date = this.state.date;
95 | const padd = date.year() % YEARS_ON_PAGE;
96 | const firstYear = date.year() - padd;
97 | for (let i = 0; i < YEARS_ON_PAGE; i++) {
98 | years[i] = (firstYear + i).toString();
99 | }
100 |
101 | return years;
102 | }
103 |
104 | protected getInitialDatePosition(): number {
105 | const selectable = this.getSelectableCellPositions();
106 | const values = this.buildCalendarValues();
107 | const currentYearIndex = values.indexOf(this.state.date.year().toString());
108 | if (selectable.indexOf(currentYearIndex) < 0) {
109 | return selectable[0];
110 | }
111 |
112 | return currentYearIndex;
113 | }
114 |
115 | protected getActiveCellPosition(): number {
116 | /*
117 | Return position of a year that should be displayed as active
118 | (position in array returned by `this.buildCalendarValues`).
119 | */
120 | if (!isNil(this.props.value)) {
121 | const years = this.buildCalendarValues();
122 | const yearIndex = years.indexOf(this.props.value.year().toString());
123 | if (yearIndex >= 0) {
124 | return yearIndex;
125 | }
126 | }
127 | }
128 |
129 | protected getSelectableCellPositions(): number[] {
130 | return filter(
131 | range(0, YEARS_ON_PAGE),
132 | (y) => !includes(this.getDisabledPositions(), y),
133 | );
134 | }
135 |
136 | protected getDisabledPositions(): number[] {
137 | /*
138 | Return position numbers of years that should be displayed as disabled
139 | (position in array returned by `this.buildCalendarValues`).
140 | */
141 | let disabled = [];
142 | const years = this.buildCalendarValues();
143 | if (isArray(this.props.enable)) {
144 | const enabledYears = this.props.enable.map((yearMoment) => yearMoment.year().toString());
145 | disabled = concat(disabled,
146 | years
147 | .filter((year) => !includes(enabledYears, year))
148 | .map((year) => years.indexOf(year)));
149 | }
150 | if (isArray(this.props.disable)) {
151 | disabled = concat(disabled,
152 | this.props.disable
153 | .filter((yearMoment) => includes(years, yearMoment.year().toString()))
154 | .map((yearMoment) => years.indexOf(yearMoment.year().toString())));
155 | }
156 | if (!isNil(this.props.maxDate)) {
157 | if (parseInt(first(years), 10) > this.props.maxDate.year()) {
158 | disabled = range(0, years.length);
159 | } else if (includes(years, this.props.maxDate.year().toString())) {
160 | disabled = concat(
161 | disabled,
162 | range(years.indexOf(this.props.maxDate.year().toString()) + 1, years.length));
163 | }
164 | }
165 | if (!isNil(this.props.minDate)) {
166 | if (parseInt(last(years), 10) < this.props.minDate.year()) {
167 | disabled = range(0, years.length);
168 | } else if (includes(years, this.props.minDate.year().toString())) {
169 | disabled = concat(
170 | disabled,
171 | range(0, years.indexOf(this.props.minDate.year().toString())));
172 | }
173 | }
174 | if (disabled.length > 0) {
175 | return uniq(disabled);
176 | }
177 | }
178 |
179 | protected isNextPageAvailable(): boolean {
180 | const {
181 | maxDate,
182 | enable,
183 | } = this.props;
184 | const lastOnPage = parseInt(last(this.buildCalendarValues()), 10);
185 |
186 | if (isArray(enable)) {
187 | return some(enable, (enabledYear) => enabledYear.year() > lastOnPage);
188 | }
189 | if (isNil(maxDate)) {
190 | return true;
191 | }
192 |
193 | return lastOnPage < maxDate.year();
194 | }
195 |
196 | protected isPrevPageAvailable(): boolean {
197 | const {
198 | minDate,
199 | enable,
200 | } = this.props;
201 | const firstOnPage = parseInt(first(this.buildCalendarValues()), 10);
202 |
203 | if (isArray(enable)) {
204 | return some(enable, (enabledYear) => enabledYear.year() < firstOnPage);
205 | }
206 | if (isNil(minDate)) {
207 | return true;
208 | }
209 |
210 | return firstOnPage > minDate.year();
211 | }
212 |
213 | protected handleChange = (e: React.SyntheticEvent, { value }): void => {
214 | const data: YearPickerOnChangeData = {
215 | ...this.props,
216 | value: { year: parseInt(value, 10) },
217 | };
218 | this.props.onChange(e, data);
219 | }
220 |
221 | protected switchToNextPage = (e: React.SyntheticEvent,
222 | data: any,
223 | callback: () => void): void => {
224 | this.setState(({ date }) => {
225 | const nextDate = date.clone();
226 | nextDate.add(YEARS_ON_PAGE, 'year');
227 |
228 | return { date: nextDate };
229 | }, callback);
230 | }
231 |
232 | protected switchToPrevPage = (e: React.SyntheticEvent,
233 | data: any,
234 | callback: () => void): void => {
235 | this.setState(({ date }) => {
236 | const prevDate = date.clone();
237 | prevDate.subtract(YEARS_ON_PAGE, 'year');
238 |
239 | return { date: prevDate };
240 | }, callback);
241 | }
242 | }
243 |
244 | export default YearPicker;
245 |
--------------------------------------------------------------------------------
/example/calendar.tsx:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 | // import 'moment/locale/ru';
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 | import {
6 | Checkbox,
7 | Form,
8 | Header,
9 | Icon,
10 | } from 'semantic-ui-react';
11 |
12 | import {
13 | DateInput,
14 | DateInputOnChangeData,
15 | DatesRangeInput,
16 | DatesRangeInputOnChangeData,
17 | DateTimeInput,
18 | DateTimeInputOnChangeData,
19 | MonthInput,
20 | MonthInputOnChangeData,
21 | MonthRangeInput,
22 | MonthRangeInputOnChangeData,
23 | TimeInput,
24 | TimeInputOnChangeData,
25 | YearInput,
26 | YearInputOnChangeData,
27 | } from '../src/inputs';
28 |
29 | type DateTimeFormHandleChangeData = DateInputOnChangeData
30 | | DatesRangeInputOnChangeData
31 | | DateTimeInputOnChangeData
32 | | MonthInputOnChangeData
33 | | MonthRangeInputOnChangeData
34 | | TimeInputOnChangeData
35 | | YearInputOnChangeData;
36 |
37 | class App extends React.Component {
38 | constructor(props) {
39 | super(props);
40 |
41 | this.state = {
42 | clearable: false,
43 | };
44 | }
45 |
46 | public render() {
47 | return (
48 |
49 |
50 | As text fields
51 |
52 |
57 |
58 |
59 |
60 |
62 |
Inline
63 |
64 |
65 | );
66 | }
67 |
68 | private handleCheckboxChange() {
69 | this.setState(() => ({
70 | clearable: !this.state.clearable,
71 | }));
72 | }
73 | }
74 |
75 | class DateTimeForm extends React.Component {
76 | constructor(props) {
77 | super(props);
78 |
79 | this.state = {
80 | year: '',
81 | date: '',
82 | dateStartYear: '',
83 | time: '',
84 | dateTime: '',
85 | datesRange: '',
86 | month: '',
87 | monthRange: '',
88 | };
89 | }
90 |
91 | public render() {
92 | const { clearable } = this.props;
93 |
94 | return (
95 |
219 | );
220 | }
221 |
222 | private handleChange = (event: React.SyntheticEvent, { name, value }: DateTimeFormHandleChangeData) => {
223 | if (this.state.hasOwnProperty(name)) {
224 | this.setState({ [name]: value });
225 | }
226 | }
227 | }
228 |
229 | class DateTimeFormInline extends React.Component {
230 | constructor(props) {
231 | super(props);
232 |
233 | this.state = {
234 | year: '',
235 | month: '',
236 | date: '',
237 | time: '',
238 | dateTime: '',
239 | datesRange: '',
240 | monthRange: '',
241 | };
242 | }
243 |
244 | public render() {
245 | return (
246 |
305 | );
306 | }
307 |
308 | private handleChange = (event: React.SyntheticEvent, { name, value }: DateTimeFormHandleChangeData) => {
309 | if (this.state.hasOwnProperty(name)) {
310 | this.setState({ [name]: value });
311 | }
312 | }
313 | }
314 |
315 | ReactDOM.render(
316 | ,
317 | document.getElementById('root'),
318 | );
319 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## v0.15.0 2019-05-04
4 |
5 | - feat: add ``hideMobileKeyboard`` prop [`#143`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/143)
6 |
7 | - feat: add ``className``'s to headers and wrap table cell content in span [`#139`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/139)
8 |
9 | - fix: Esc and Arrow keys fixes for Internet Explorer. And removed prevent default on arrow left/right events [`#137`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/137)
10 |
11 | - fix(DatesRangeInput): error when allowed range doesn't contain today [`#140`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/140)
12 |
13 | - fix (DateInput, DateTimeInput): update internal *Input date when value changed [`#142`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/142)
14 |
15 | ## v0.14.4 2019-03-30
16 |
17 | - fix(package): set proper types path [`#115`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/115)
18 |
19 | ## v0.14.3 2019-03-17
20 |
21 | - feat(DatesRangeInput): allow same start/end date selection when using [`#104`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/104)
22 |
23 | - fix: readOnly prop in InputView [`fe63e3d`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/fe63e3d35c22b62ef23511afba47f56346d03187)
24 |
25 | - fix: #95 #55 can't change years and months in IE [`#110`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/110)
26 |
27 | - chore: build for multiple module systems [`ef32ca7`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/ef32ca7b900a6d83245f84a6be06c1eb84c4a13f)
28 |
29 | - chore: add umd build [`#111`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/111)
30 |
31 | ## v0.14.2 2019-02-27
32 |
33 | - fix: can mark any date [`#98`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/98)
34 |
35 | - fix(InputView): don't wrap field in extra div [`#99`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/99)
36 |
37 | - fix: use "ui icon input" for correct styling [`#100`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/100)
38 |
39 | - fix: can remove icon from input [`#101`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/101)
40 |
41 | ## v0.14.1 2019-02-19
42 |
43 | - feat: allow to mark specific dates [`#77`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/77)
44 |
45 | - feat: MonthRangeInput [`#79`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/79)
46 |
47 | - feat: adds localization prop on each component [`#85`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/85)
48 |
49 | - fix(MinutePicker): #78 minDate disables minutes in each hour in the day [`93972b3`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/93972b3077b2957fb3e4d1f9ecd2e087e3fa4b3d)
50 |
51 | - fix: #83 pass popup mount node to inputView [`#89`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/86)
52 |
53 | - fix: #84 initialize picker state with input value [`#88`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/88)
54 |
55 | - fix(DateInput/DateTimeInput): #93 able to change year, month, date ... [`#96`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/96)
56 |
57 | ## v0.13.0 2019-01-12
58 |
59 | - feat: added transitions for popup [`65`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/65)
60 |
61 | - feat: pickerWidth and pickerStyle props on top level component [`6ed8e76`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/6ed8e76207012c11eae705c6d79de14e4b42623b)
62 |
63 | - fix: BasePicker SyntheticEvent generic type [`69`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/69)
64 |
65 | ## v0.12.2 2018-12-31
66 |
67 | - feat: add clearable props to Input [`#60`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/60)
68 |
69 | - fix: do not select disabled cells after page switch [`b536d89`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/b536d89e8af52e533c97735a0301a0c4dfd04963)
70 |
71 | - fix: not jump over 0th cell on ArrowLeft press [`394470c`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/394470c1105400ca3f62858dc0856da4125c047b)
72 |
73 | - fix(MinutePicker): getInitialDatePosition handles disabled positions [`c1ad726`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/c1ad72661e8d5a88efeacf5573ecfd2e9104bff8)
74 |
75 | - fix: #59 prevent selecting disabled values [`bab7718`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/bab7718df3f969e4deb6001517c14b8ac6bb6137)
76 |
77 | ## v0.12.1 2018-11-24
78 |
79 | - fix: stale input node reference [`32b56c3`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/32b56c381891bd716efb3a93e1ef8ef1ac0400a6)
80 |
81 | - fix: jump over disabled cell when keyboard navigating [`9c15bb1`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/9c15bb17505ea536c71df8d351a9c01441c635c6)
82 |
83 | - fix: if date in month/year disabled the whole month/year disabled [`ee9b673`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/ee9b673a981c436550f7fd3216d7129f2b9fd707)
84 |
85 | - fix: string value in `disable` prop doesn't work [`7ce6c73`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/7ce6c73b017fddd35534c2cb4b3b8433895074ec)
86 |
87 | ## v0.12.0 2018-11-19
88 |
89 | - feat: add disableMinute prop to TimeInput (#49) [`#49`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/49)
90 |
91 | - feat: keyboard shortcuts support [`0033d62`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/0033d62a8061c3cd1d2d9ff0fad7b0e17b0167a2)
92 |
93 | - fix: popup closes on selection [`e3d1807`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/e3d1807d810c06ff32936ab5c4f3ea4aedf12f53)
94 |
95 | - fix: extra Tab needed to navigate inside calendar [`5acc549`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/5acc5491de046b80fb3b444b3a664f327a1e15f2)
96 |
97 | - fix: remove on focus outline from poped up picker [`550f1a4`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/550f1a494b904811707459932314ad864dd815e8)
98 |
99 | ## v0.11.0 2018-11-03
100 |
101 | - feat: add dateTimeFormat prop to DateTimePicker (#42) [`#42`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/42)
102 |
103 | - fix(yearPicker): initialize page with selected value [`6c639aa`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/6c639aa70b53a8c7a56e83c24fdcab8c4aec2aff)
104 |
105 | - fix: #28 popup blured when inside Modal [`036a95f`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/036a95f052aefacfaf97afa66cdf09a8598c969a)
106 |
107 | - fix: initialDate prevent clearing input field [`8c51722`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/8c51722c670bf0b2a8beedb68550a2ec9b797e2d)
108 |
109 | ## v0.10.0 2018-10-18
110 |
111 | - feat: allow passthrough of mountNode to InputView Popup (#38) [`#38`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/38)
112 |
113 | - fix: #39 invalid date when first week in Jan starts with day from prev month (#40) [`#40`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/40)
114 |
115 | ## v0.9.1 2018-09-30
116 |
117 | - feat(preserveViewMode): allow preserveViewMode to reset mode onFocus [`#36`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/36)
118 |
119 | ## v0.9.0 2018-09-18
120 |
121 | - fix: #31 min/maxDate params not working [`b9f335f`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/b9f335f3b8e234549a9c2a144ba277b50bd5a5fe)
122 | - fix: #34 calendar popup unexpectedly closes [`5edea86`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/5edea86ccc9ac27e5af4aa9fb37b95b59a61e95b)
123 | - fix: #33 initialDate doesn't work [`d15f374`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/d15f374b15a181e092561bf959e1986188bda3c1)
124 | - fix: delay handle change on one tick [`4e012f4`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/4e012f4dfdf93d3767b1a84116985a08458ec6a6)
125 | - fix: weeks labels dont change locale [`24b0632`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/24b0632ac2b96bc0db864eb9f285bfb99ac2df6e)
126 |
127 | - feat(DateInput): `enable` attribute #30 [`53c19c3`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/53c19c351a3a867ef8f7a0e50bb92c407543cf28)
128 |
129 | ## v0.8.0 2018-08-04
130 |
131 | - feat: `closeOnMouseLeave` prop [`#23`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/23)
132 |
133 | - fix: #20 onClick prop got false instead of undefined [`#21`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/21)
134 |
135 | - breaking: use Form.Input instead of Input [`abda4fb`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/abda4fb9059dc68ec09da3072e3e1d86463d58b1)
136 |
137 |
138 | ## v0.7.1 2018-07-22
139 |
140 | - feat: `disable`, `minDate`, `maxDate` attributes [`af0d3a9`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/af0d3a91933903f5fc82fee83e5a0499f44f544f)
141 | - feat: add `initialDate` attribute [`23e8008`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/23e800851716e0645451c99f2e0084937747a4c6)
142 |
143 | - fix(DatesRangeInput): clear selected range if `value` is empty [`3b57013`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/3b57013f3f8bd56092c7612f965894f4efc5109e)
144 | - fix(DateInput): accidental import couses error [`45b9811`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/45b9811e6f780d4df4170bc0aca3ab3171f4539f)
145 |
--------------------------------------------------------------------------------
/src/views/InputView.tsx:
--------------------------------------------------------------------------------
1 | import isString from 'lodash/isString';
2 | import invoke from 'lodash/invoke';
3 |
4 | import * as React from 'react';
5 | import {
6 | Form,
7 | FormInputProps,
8 | Icon,
9 | Popup,
10 | SemanticICONS,
11 | SemanticTRANSITIONS,
12 | Transition,
13 | } from 'semantic-ui-react';
14 | import checkIE from '../lib/checkIE';
15 | import checkMobile from '../lib/checkMobile';
16 |
17 | const popupStyle = {
18 | padding: '0',
19 | filter: 'none', // prevents bluring popup when used inside Modal with dimmer="bluring" #28 #26
20 | };
21 |
22 | class FormInputWithRef extends React.Component {
23 |
24 | public render() {
25 |
26 | const {
27 | value,
28 | clearable,
29 | icon,
30 | clearIcon,
31 | onClear,
32 | innerRef,
33 | onFocus,
34 | onBlur,
35 | onMouseEnter,
36 | ...rest
37 | } = this.props;
38 |
39 | const ClearIcon = isString(clearIcon) ?
40 | :
41 |
42 | ;
43 |
44 | return (
45 |
53 | {value && clearable ?
54 | ClearIcon
55 | :
56 |
57 | }
58 |
59 |
63 |
64 |
65 | );
66 | }
67 | }
68 |
69 | interface InputViewProps {
70 | /** Used for passing input dom node (input field or inline calendar) to parent component. */
71 | onMount: (e: HTMLElement) => void;
72 | /** Called after input field value has changed. */
73 | onChange: (e: React.SyntheticEvent, data: any) => void;
74 | /** Called when component looses focus. */
75 | onBlur?: (e: React.SyntheticEvent) => void;
76 | closePopup: () => void;
77 | openPopup: () => void;
78 | /** Called on input focus. */
79 | onFocus?: () => void;
80 | /** Function for rendering picker. */
81 | renderPicker: () => React.ReactNode;
82 | /** Called after clear icon has clicked. */
83 | onClear?: (e: React.SyntheticEvent, data: any) => void;
84 | /** Whether to close a popup when cursor leaves it. */
85 | closeOnMouseLeave?: boolean;
86 | /** A field can have its label next to instead of above it. */
87 | inlineLabel?: boolean;
88 | /** Using the clearable setting will let users remove their selection from a calendar. */
89 | clearable?: boolean;
90 | /** Optional Icon to display inside the Input. */
91 | icon?: SemanticICONS | boolean;
92 | /** Icon position. Default: 'right'. */
93 | iconPosition?: 'left' | 'right';
94 | /** Optional Icon to display inside the clearable Input. */
95 | clearIcon?: any;
96 | /** Whether popup is closed. */
97 | popupIsClosed?: boolean;
98 | /** The node where the picker should mount. */
99 | mountNode?: HTMLElement;
100 | /** Input element tabindex. */
101 | tabIndex?: string | number;
102 | /** Whether to display inline picker or picker inside a popup. */
103 | inline?: boolean;
104 | /** Duration of the CSS transition animation in milliseconds. */
105 | duration?: number;
106 | /** Named animation event to used. Must be defined in CSS. */
107 | animation?: SemanticTRANSITIONS;
108 | /** Where to display popup. */
109 | popupPosition?:
110 | | 'top left'
111 | | 'top right'
112 | | 'bottom right'
113 | | 'bottom left'
114 | | 'right center'
115 | | 'left center'
116 | | 'top center'
117 | | 'bottom center';
118 | /** Currently selected value. */
119 | value?: string;
120 | /** Picker width (any value that `style.width` can take). */
121 | pickerWidth?: string;
122 | /** Style object for picker. */
123 | pickerStyle?: object;
124 | /** Do not display popup if true. */
125 | readOnly?: boolean;
126 | /** Try to prevent mobile keyboard appearing. */
127 | hideMobileKeyboard?: boolean;
128 | }
129 |
130 | class InputView extends React.Component {
131 | public static defaultProps = {
132 | inline: false,
133 | closeOnMouseLeave: true,
134 | tabIndex: '0',
135 | clearable: false,
136 | clearIcon: 'remove',
137 | animation: 'scale',
138 | duration: 200,
139 | iconPosition: 'right',
140 | };
141 |
142 | private inputNode: HTMLElement | undefined;
143 | private popupNode: HTMLElement | undefined;
144 | private mouseLeaveTimeout: number | null;
145 |
146 | public onBlur = (e, ...args) => {
147 | const {
148 | closePopup,
149 | } = this.props;
150 |
151 | if (
152 | e.relatedTarget !== this.popupNode
153 | && e.relatedTarget !== this.inputNode
154 | && !checkIE()
155 | ) {
156 | invoke(this.props, 'onBlur', e, ...args);
157 | closePopup();
158 | }
159 | }
160 |
161 | public onMouseLeave = (e, ...args) => {
162 | const { closeOnMouseLeave, closePopup } = this.props;
163 |
164 | if (e.relatedTarget !== this.popupNode && e.relatedTarget !== this.inputNode) {
165 | if (closeOnMouseLeave) {
166 | invoke(this.props, 'onMouseLeave', e, ...args);
167 | this.mouseLeaveTimeout = window.setTimeout(() => {
168 | if (this.mouseLeaveTimeout) {
169 | closePopup();
170 | }
171 | }, 500);
172 | }
173 | }
174 | }
175 |
176 | public onMouseEnter = (e, ...args) => {
177 | const { closeOnMouseLeave } = this.props;
178 |
179 | invoke(this.props, 'onMouseEnter', e, ...args);
180 | if (e.currentTarget === this.popupNode || e.currentTarget === this.inputNode) {
181 | if (closeOnMouseLeave) {
182 | clearTimeout(this.mouseLeaveTimeout);
183 | this.mouseLeaveTimeout = null;
184 | }
185 | }
186 | }
187 |
188 | public render() {
189 | const {
190 | renderPicker,
191 | popupPosition,
192 | inline,
193 | value,
194 | closeOnMouseLeave,
195 | onChange,
196 | onClear,
197 | children,
198 | inlineLabel,
199 | popupIsClosed,
200 | mountNode,
201 | tabIndex,
202 | onMount,
203 | closePopup,
204 | openPopup,
205 | animation,
206 | duration,
207 | pickerWidth,
208 | pickerStyle,
209 | iconPosition,
210 | icon,
211 | readOnly,
212 | hideMobileKeyboard,
213 | ...rest
214 | } = this.props;
215 |
216 | const inputElement = (
217 | { this.inputNode = e; onMount(e); }}
224 | value={value}
225 | tabIndex={tabIndex}
226 | inline={inlineLabel}
227 | onClear={(e) => (onClear || onChange)(e, { ...rest, value: '' })}
228 | onFocus={(e) => {
229 | invoke(this.props, 'onFocus', e, this.props);
230 | openPopup();
231 | }}
232 | onBlur={this.onBlur}
233 | onMouseEnter={this.onMouseEnter}
234 | onChange={onChange} />
235 | );
236 |
237 | if (inline) {
238 | return renderPicker();
239 | }
240 |
241 | return (<>
242 | {inputElement}
243 | {
244 | !readOnly
245 | &&
246 | {
253 | if (popupIsClosed) {
254 | this.unsetScrollListener();
255 | // TODO: for some reason sometimes transition component
256 | // doesn't hide even though `popupIsClosed === true`
257 | // To hide it we need to rerender component
258 | this.forceUpdate();
259 | } else {
260 | this.setScrollListener();
261 | }
262 | }}
263 | >
264 |
274 | this.popupNode = ref}
281 | >
282 | {renderPicker()}
283 |
284 |
285 |
286 | }
287 | >
288 | );
289 | }
290 |
291 | public scrollListener = () => {
292 | const { closePopup } = this.props;
293 | closePopup();
294 | }
295 |
296 | private setScrollListener() {
297 | window.addEventListener('scroll', this.scrollListener);
298 | }
299 |
300 | private unsetScrollListener() {
301 | window.removeEventListener('scroll', this.scrollListener);
302 | }
303 | }
304 |
305 | export default InputView;
306 |
--------------------------------------------------------------------------------
/src/inputs/DateInput.tsx:
--------------------------------------------------------------------------------
1 | import isNil from 'lodash/isNil';
2 | import invoke from 'lodash/invoke';
3 | import moment, { Moment } from 'moment';
4 | import * as PropTypes from 'prop-types';
5 | import * as React from 'react';
6 |
7 | import {
8 | BasePickerOnChangeData,
9 | } from '../pickers/BasePicker';
10 | import DayPicker from '../pickers/dayPicker/DayPicker';
11 | import MonthPicker from '../pickers/monthPicker/MonthPicker';
12 | import YearPicker from '../pickers/YearPicker';
13 | import InputView from '../views/InputView';
14 | import BaseInput, {
15 | BaseInputProps,
16 | BaseInputPropTypes,
17 | BaseInputState,
18 | DateRelatedProps,
19 | DateRelatedPropTypes,
20 | DisableValuesProps,
21 | DisableValuesPropTypes,
22 | EnableValuesProps,
23 | EnableValuesPropTypes,
24 | MinMaxValueProps,
25 | MinMaxValuePropTypes,
26 | MultimodeProps,
27 | MultimodePropTypes,
28 | MarkedValuesProps,
29 | MarkedValuesPropTypes,
30 | } from './BaseInput';
31 |
32 | import {
33 | tick,
34 | } from '../lib';
35 | import {
36 | buildValue,
37 | parseArrayOrValue,
38 | parseValue,
39 | dateValueToString,
40 | } from './parse';
41 | import {
42 | getDisabledMonths, getDisabledYears,
43 | } from './shared';
44 |
45 | type CalendarMode = 'year' | 'month' | 'day';
46 |
47 | function getNextMode(currentMode: CalendarMode) {
48 | if (currentMode === 'year') {
49 | return 'month';
50 | }
51 | if (currentMode === 'month') {
52 | return 'day';
53 | }
54 |
55 | return 'year';
56 | }
57 |
58 | function getPrevMode(currentMode: CalendarMode) {
59 | if (currentMode === 'day') {
60 | return 'month';
61 | }
62 | if (currentMode === 'month') {
63 | return 'year';
64 | }
65 |
66 | return 'day';
67 | }
68 |
69 | export interface DateInputProps extends
70 | BaseInputProps,
71 | DateRelatedProps,
72 | MultimodeProps,
73 | DisableValuesProps,
74 | EnableValuesProps,
75 | MarkedValuesProps,
76 | MinMaxValueProps {
77 | /** Display mode to start. */
78 | startMode?: CalendarMode;
79 | }
80 |
81 | export type DateInputOnChangeData = DateInputProps;
82 |
83 | interface DateInputState extends BaseInputState {
84 | mode: CalendarMode;
85 | year: number;
86 | month: number;
87 | date: number;
88 | }
89 |
90 | class DateInput extends BaseInput {
91 | /**
92 | * Component responsibility:
93 | * - parse input value
94 | * - handle underlying picker change
95 | */
96 | public static readonly defaultProps = {
97 | ...BaseInput.defaultProps,
98 | dateFormat: 'DD-MM-YYYY',
99 | startMode: 'day',
100 | preserveViewMode: true,
101 | icon: 'calendar',
102 | };
103 |
104 | public static readonly propTypes = {
105 | ...BaseInputPropTypes,
106 | ...DateRelatedPropTypes,
107 | ...MultimodePropTypes,
108 | ...DisableValuesPropTypes,
109 | ...EnableValuesPropTypes,
110 | ...MarkedValuesPropTypes,
111 | ...MinMaxValuePropTypes,
112 | ...{
113 | /** Display mode to start. */
114 | startMode: PropTypes.oneOf([ 'year', 'month', 'day' ]),
115 | },
116 | };
117 |
118 | constructor(props: DateInputProps) {
119 | super(props);
120 | const parsedValue = parseValue(props.value, props.dateFormat, props.localization);
121 | this.state = {
122 | mode: props.startMode,
123 | popupIsClosed: true,
124 | year: parsedValue ? parsedValue.year() : undefined,
125 | month: parsedValue ? parsedValue.month() : undefined,
126 | date: parsedValue ? parsedValue.date() : undefined,
127 | };
128 | }
129 |
130 | public componentDidUpdate = (prevProps: DateInputProps) => {
131 | // update internal date if ``value`` prop changed and successuffly parsed
132 | if (prevProps.value !== this.props.value) {
133 | const parsed = parseValue(this.props.value, this.props.dateFormat, this.props.localization);
134 | if (parsed) {
135 | this.setState({
136 | year: parsed.year(),
137 | month: parsed.month(),
138 | date: parsed.date(),
139 | });
140 | }
141 | }
142 | }
143 |
144 | public render() {
145 | const {
146 | value,
147 | dateFormat,
148 | initialDate,
149 | disable,
150 | enable,
151 | maxDate,
152 | minDate,
153 | preserveViewMode,
154 | startMode,
155 | closable,
156 | markColor,
157 | marked,
158 | localization,
159 | onChange,
160 | ...rest
161 | } = this.props;
162 |
163 | return (
164 | this.getPicker()}
173 | value={dateValueToString(value, dateFormat, localization)}
174 | />
175 | );
176 | }
177 |
178 | private parseInternalValue(): Moment {
179 | /*
180 | Creates moment instance from values stored in component's state
181 | (year, month, date) in order to pass this moment instance to
182 | underlying picker.
183 | Return undefined if none of these state fields has value.
184 | */
185 | const {
186 | year,
187 | month,
188 | date,
189 | } = this.state;
190 | if (!isNil(year) || !isNil(month) || !isNil(date)) {
191 | return moment({ year, month, date });
192 | }
193 | }
194 |
195 | private getPicker = () => {
196 | const {
197 | value,
198 | initialDate,
199 | dateFormat,
200 | disable,
201 | minDate,
202 | maxDate,
203 | enable,
204 | inline,
205 | marked,
206 | markColor,
207 | localization,
208 | tabIndex,
209 | pickerWidth,
210 | pickerStyle,
211 | } = this.props;
212 | const pickerProps = {
213 | isPickerInFocus: this.isPickerInFocus,
214 | isTriggerInFocus: this.isTriggerInFocus,
215 | inline,
216 | onCalendarViewMount: this.onCalendarViewMount,
217 | closePopup: this.closePopup,
218 | tabIndex,
219 | pickerWidth,
220 | pickerStyle,
221 | onChange: this.handleSelect,
222 | onHeaderClick: this.switchToPrevMode,
223 | initializeWith: buildValue(this.parseInternalValue(), initialDate, localization, dateFormat),
224 | value: buildValue(value, null, localization, dateFormat, null),
225 | enable: parseArrayOrValue(enable, dateFormat, localization),
226 | minDate: parseValue(minDate, dateFormat, localization),
227 | maxDate: parseValue(maxDate, dateFormat, localization),
228 | localization,
229 | };
230 | const disableParsed = parseArrayOrValue(disable, dateFormat, localization);
231 | const markedParsed = parseArrayOrValue(marked, dateFormat, localization);
232 | const { mode } = this.state;
233 | if (mode === 'year') {
234 | return (
235 |
239 | );
240 | }
241 | if (mode === 'month') {
242 | return (
243 |
248 | );
249 | }
250 |
251 | return ;
252 | }
253 |
254 | private switchToNextModeUndelayed = (): void => {
255 | this.setState(({ mode }) => {
256 | return { mode: getNextMode(mode) };
257 | }, this.onModeSwitch);
258 | }
259 |
260 | private switchToNextMode = (): void => {
261 | tick(this.switchToNextModeUndelayed);
262 | }
263 |
264 | private switchToPrevModeUndelayed = (): void => {
265 | this.setState(({ mode }) => {
266 | return { mode: getPrevMode(mode) };
267 | }, this.onModeSwitch);
268 | }
269 |
270 | private switchToPrevMode = (): void => {
271 | tick(this.switchToPrevModeUndelayed);
272 | }
273 |
274 | private onFocus = (): void => {
275 | if (!this.props.preserveViewMode) {
276 | this.setState({ mode: this.props.startMode });
277 | }
278 | }
279 |
280 | private handleSelect = (e, { value }: BasePickerOnChangeData) => {
281 | if (this.state.mode === 'day' && this.props.closable) {
282 | this.closePopup();
283 | }
284 | this.setState((prevState) => {
285 | const {
286 | mode,
287 | } = prevState;
288 | if (mode === 'day') {
289 | const outValue = moment(value).format(this.props.dateFormat);
290 | invoke(this.props, 'onChange', e, { ...this.props, value: outValue });
291 | }
292 |
293 | return {
294 | year: value.year,
295 | month: value.month,
296 | date: value.date,
297 | };
298 | }, () => this.state.mode !== 'day' && this.switchToNextMode());
299 | }
300 |
301 | /** Keeps internal state in sync with input field value. */
302 | private onInputValueChange = (e, { value }) => {
303 | const parsedValue = moment(value, this.props.dateFormat);
304 | if (parsedValue.isValid()) {
305 | this.setState({
306 | year: parsedValue.year(),
307 | month: parsedValue.month(),
308 | date: parsedValue.date(),
309 | });
310 | }
311 | invoke(this.props, 'onChange', e, { ...this.props, value });
312 | }
313 | }
314 |
315 | export default DateInput;
316 |
--------------------------------------------------------------------------------
/test/views/testHeader.js:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import * as Enzyme from 'enzyme';
3 | import * as Adapter from 'enzyme-adapter-react-16';
4 | import moment from 'moment';
5 | import {
6 | shallow,
7 | } from 'enzyme';
8 | import * as sinon from 'sinon';
9 | import * as React from 'react';
10 | import {
11 | Table,
12 | Icon,
13 | } from 'semantic-ui-react';
14 | import * as _ from 'lodash';
15 |
16 | import {
17 | Header,
18 | HeaderRange,
19 | HeaderWeeks,
20 | } from '../../src/views/CalendarHeader';
21 |
22 | describe('', () => {
23 | it('consists of proper elements', () => {
24 | const wrapper = shallow();
25 | assert(wrapper.is(Table.Row), 'the top node is ');
26 | assert(wrapper.children().every(Table.HeaderCell), 'top node contains nodes: ');
27 | assert.equal(wrapper.children().getElements().length, 7, 'top node contains 7 nodes');
28 | });
29 |
30 | it('has proper localization', () => {
31 | const wrapper = shallow();
32 | const weekArray = wrapper.children().getElements().map(element => moment(element.key, 'ddd dddd', 'ru').isValid());
33 | assert.notInclude(weekArray, false, 'days of the week parsed correctly');
34 | assert.equal(weekArray.length, 7, 'top node contains 7 nodes');
35 | });
36 |
37 | it('has proper styling', () => {
38 | const wrapper = shallow();
39 | wrapper.children().forEach((child) => {
40 | const style = child.prop('style');
41 | assert.equal(_.keys(style).length, 2, 'each child of top node has prop style: { border, borderBottom }');
42 | assert.equal(
43 | style.border,
44 | 'none',
45 | 'each child of top node has prop style: { border: "none" }');
46 | assert.equal(
47 | style.borderBottom,
48 | '1px solid rgba(34,36,38,.1)',
49 | 'each child of top node has prop style: { borderBottom: "1px solid rgba(34,36,38,.1)" }');
50 | });
51 | });
52 | });
53 |
54 | describe('', () => {
55 | it('consists of proper elements and sets its content correctly', () => {
56 | const wrapper = shallow();
57 | assert(wrapper.is(Table.Row), 'the top node is ');
58 | assert(wrapper.children().first().is(Table.HeaderCell), 'top node contains ');
59 | assert.equal(wrapper.children().getElements().length, 1, 'top node contains just one ');
60 | assert.equal(wrapper.find(Table.HeaderCell).children().first().text(), 'any text', 'uses `content` prop as it should');
61 | });
62 |
63 | it('has proper styling', () => {
64 | const wrapper = shallow();
65 | wrapper.children().forEach((child) => {
66 | const style = child.prop('style');
67 | assert.equal(_.keys(style).length, 1, 'each child of top node has prop style: { border: "none" }');
68 | assert.equal(style.border, 'none', 'each child of top node has prop style: { border: "none" }');
69 | });
70 | const style = wrapper.children().first().prop('style');
71 | assert.equal(_.keys(style).length, 1, 'top node\'s child has prop style: { border: "none" }');
72 | assert.equal(style.border, 'none', 'top node\'s child has prop style: { border: "none" }');
73 | });
74 | });
75 |
76 | describe('', () => {
77 | it('consists of proper elements', () => {
78 | const wrapper = shallow(
79 | {}}
86 | onPrevPageBtnClick={() => {}} />
87 | );
88 | assert(wrapper.is(Table.Header), 'top node is Table.Header');
89 | assert.equal(wrapper.find(Table.Row).getElements().length, 1, 'has one ');
90 | assert.equal(wrapper.find(HeaderWeeks).getElements().length, 1, 'has one ');
91 | assert.isFalse(wrapper.find(HeaderRange).exists(), 'does not have ');
92 | });
93 |
94 | it('sets title properly', () => {
95 | const wrapper = shallow(
96 | {}}
103 | onPrevPageBtnClick={() => {}} />
104 | );
105 | assert.equal(wrapper.find(Table.HeaderCell)
106 | .at(1).children().first().text(), 'any text', 'node contains value from `title` prop');
107 | });
108 |
109 | it('does not display weeks row if `displayWeeks` is false', () => {
110 | const wrapper = shallow(
111 | {}}
118 | onPrevPageBtnClick={() => {}} />
119 | );
120 | assert.isFalse(wrapper.find(HeaderWeeks).exists(), 'does not have ');
121 | });
122 |
123 | it('display range row if `rangeRowContent` provided', () => {
124 | const wrapper = shallow(
125 | {}}
132 | onPrevPageBtnClick={() => {}}
133 | rangeRowContent="any text" />
134 | );
135 | assert(wrapper.find(HeaderRange).exists(), 'has ');
136 | });
137 |
138 | it('sets central cell colSpan to 5 if `width` 7', () => {
139 | const wrapper = shallow(
140 | {}}
147 | onPrevPageBtnClick={() => {}} />
148 | );
149 | assert.equal(wrapper.find(Table.HeaderCell)
150 | .at(1).prop('colSpan'), (7 - 2).toString(), 'central cell colSpan === (7 - 2)');
151 | });
152 |
153 | it('sets central cell colSpan to 2 if `width` 4', () => {
154 | const wrapper = shallow(
155 | {}}
162 | onPrevPageBtnClick={() => {}} />
163 | );
164 | assert.equal(wrapper.find(Table.HeaderCell)
165 | .at(1).prop('colSpan'), (4 - 2).toString(), 'central cell colSpan === (4 - 2)');
166 | });
167 |
168 | it('sets central cell colSpan to 1 if `width` 3', () => {
169 | const wrapper = shallow(
170 | {}}
177 | onPrevPageBtnClick={() => {}} />
178 | );
179 | assert.equal(wrapper.find(Table.HeaderCell)
180 | .at(1).prop('colSpan'), (3 - 2).toString(), 'central cell colSpan === (3 - 2)');
181 | });
182 |
183 | it('calls onPrevPageBtnClick', () => {
184 | const onPrevPageBtnClick = sinon.fake();
185 | const onNextPageBtnClick = sinon.fake();
186 | const wrapper = shallow(
187 |
195 | );
196 | const prevBtn = wrapper.find(Icon).first();
197 | prevBtn.simulate('click');
198 | assert(onPrevPageBtnClick.calledOnce, 'onPrevPageBtnClick is called');
199 | });
200 |
201 | it('calls onNextPageBtnClick', () => {
202 | const onPrevPageBtnClick = sinon.fake();
203 | const onNextPageBtnClick = sinon.fake();
204 | const wrapper = shallow(
205 |
213 | );
214 | const nextBtn = wrapper.find(Icon).last();
215 | nextBtn.simulate('click');
216 | assert(onNextPageBtnClick.calledOnce, 'onNextPageBtnClick is called');
217 | });
218 |
219 | it('does not call onNextPageBtnClick if it has not next page', () => {
220 | const onPrevPageBtnClick = sinon.fake();
221 | const onNextPageBtnClick = sinon.fake();
222 | const wrapper = shallow(
223 |
231 | );
232 | const nextBtn = wrapper.find(Icon).last();
233 | nextBtn.simulate('click');
234 | assert(onNextPageBtnClick.notCalled, 'onNextPageBtnClick is not called');
235 | });
236 |
237 | it('does not call onPrevPageBtnClick if it has not previous page', () => {
238 | const onPrevPageBtnClick = sinon.fake();
239 | const onNextPageBtnClick = sinon.fake();
240 | const wrapper = shallow(
241 |
249 | );
250 | const prevBtn = wrapper.find(Icon).first();
251 | prevBtn.simulate('click');
252 | assert(onPrevPageBtnClick.notCalled, 'onPrevPageBtnClick is not called');
253 | });
254 |
255 | it('calls onHeaderClick after clicking on header if `onHeaderClick` prop provided', () => {
256 | const onHeaderClick = sinon.fake();
257 | const wrapper = shallow(
258 | {}}
266 | onPrevPageBtnClick={() => {}} />
267 | );
268 | wrapper.find(Table.HeaderCell).at(1).simulate('click');
269 | assert(onHeaderClick.calledOnce, 'onHeaderClick is called once');
270 | });
271 | });
272 |
--------------------------------------------------------------------------------
/test/pickers/timePicker/testMinutePicker.js:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import {
3 | mount,
4 | } from 'enzyme';
5 | import * as sinon from 'sinon';
6 | import * as React from 'react';
7 | import * as _ from 'lodash';
8 | import moment from 'moment';
9 |
10 | import MinutePicker from '../../../src/pickers/timePicker/MinutePicker';
11 |
12 | describe('', () => {
13 | it('initialized with moment', () => {
14 | const date = moment('2015-05-01');
15 | const wrapper = mount();
16 | assert(
17 | moment.isMoment(wrapper.state('date')),
18 | 'has moment instance in `date` state field');
19 | assert(
20 | wrapper.state('date').isSame(date),
21 | 'initialize `date` state field with moment provided in `initializeWith` prop');
22 | });
23 | });
24 |
25 | describe(': buildCalendarValues', () => {
26 | const date = moment('2018-08-12 15:00');
27 |
28 | describe('`timeFormat` not provided', () => {
29 | it('return array of strings', () => {
30 | const wrapper = mount();
31 | const shouldReturn = [
32 | '15:00', '15:05', '15:10', '15:15', '15:20', '15:25',
33 | '15:30', '15:35', '15:40', '15:45', '15:50', '15:55',
34 | ];
35 | assert(_.isArray(wrapper.instance().buildCalendarValues()), 'return array');
36 | assert.equal(wrapper.instance().buildCalendarValues().length, 12, 'return array of length 12');
37 | wrapper.instance().buildCalendarValues().forEach((minutePosition, i) => {
38 | assert.equal(minutePosition, shouldReturn[i], 'contains corect minute positions');
39 | });
40 | });
41 | });
42 |
43 | describe('`timeFormat` is ampm', () => {
44 | it('return array of strings', () => {
45 | const wrapper = mount();
48 | const shouldReturn = [
49 | '03:00 pm', '03:05 pm', '03:10 pm', '03:15 pm', '03:20 pm', '03:25 pm',
50 | '03:30 pm', '03:35 pm', '03:40 pm', '03:45 pm', '03:50 pm', '03:55 pm',
51 | ];
52 | assert(_.isArray(wrapper.instance().buildCalendarValues()), 'return array');
53 | assert.equal(wrapper.instance().buildCalendarValues().length, 12, 'return array of length 12');
54 | wrapper.instance().buildCalendarValues().forEach((minutePosition, i) => {
55 | assert.equal(minutePosition, shouldReturn[i], 'contains corect minute positions');
56 | });
57 | });
58 | });
59 |
60 | describe('`timeFormat` is AMPM', () => {
61 | it('return array of strings', () => {
62 | const wrapper = mount();
65 | const shouldReturn = [
66 | '03:00 PM', '03:05 PM', '03:10 PM', '03:15 PM', '03:20 PM', '03:25 PM',
67 | '03:30 PM', '03:35 PM', '03:40 PM', '03:45 PM', '03:50 PM', '03:55 PM',
68 | ];
69 | assert(_.isArray(wrapper.instance().buildCalendarValues()), 'return array');
70 | assert.equal(wrapper.instance().buildCalendarValues().length, 12, 'return array of length 12');
71 | wrapper.instance().buildCalendarValues().forEach((minutePosition, i) => {
72 | assert.equal(minutePosition, shouldReturn[i], 'contains corect minute positions');
73 | });
74 | });
75 | });
76 | });
77 |
78 | describe(': getActiveCellPosition', () => {
79 | const date = moment('2018-08-12 10:00');
80 |
81 | it('return active minute position when value is not multiple of 5', () => {
82 | const wrapper = mount();
85 | /*
86 | [
87 | '10:00', '10:05', '10:10', '10:15', '10:20', '10:25',
88 | '10:30', '10:35', '10:40', '10:45', '10:50', '10:55',
89 | ]
90 | */
91 | assert(_.isNumber(wrapper.instance().getActiveCellPosition()), 'return number');
92 | assert.equal(wrapper.instance().getActiveCellPosition(), 3, 'return active minute position number');
93 | });
94 |
95 | it('return active minute position when value is multiple of 5', () => {
96 | const wrapper = mount();
99 | /*
100 | [
101 | '10:00', '10:05', '10:10', '10:15', '10:20', '10:25',
102 | '10:30', '10:35', '10:40', '10:45', '10:50', '10:55',
103 | ]
104 | */
105 | assert(_.isNumber(wrapper.instance().getActiveCellPosition()), 'return number');
106 | assert.equal(wrapper.instance().getActiveCellPosition(), 4, 'return active minute position number');
107 | });
108 |
109 | it('return active minute position when value is 59', () => {
110 | const wrapper = mount();
113 | /*
114 | [
115 | '10:00', '10:05', '10:10', '10:15', '10:20', '10:25',
116 | '10:30', '10:35', '10:40', '10:45', '10:50', '10:55',
117 | ]
118 | */
119 | assert(_.isNumber(wrapper.instance().getActiveCellPosition()), 'return number');
120 | assert.equal(wrapper.instance().getActiveCellPosition(), 11, 'return active minute position number');
121 | });
122 |
123 | it('return undefined when value is not provided', () => {
124 | const wrapper = mount();
126 | assert(_.isUndefined(wrapper.instance().getActiveCellPosition()), 'return undefined');
127 | });
128 | });
129 |
130 | describe(': isNextPageAvailable', () => {
131 | const date = moment('2018-08-12');
132 |
133 | describe('is not available by maxDate', () => {
134 | it('return false', () => {
135 | const wrapper = mount();
138 |
139 | assert(_.isBoolean(wrapper.instance().isNextPageAvailable()), 'return boolean');
140 | assert.isFalse(wrapper.instance().isNextPageAvailable(), 'return false');
141 | });
142 | });
143 |
144 | describe('available by maxDate', () => {
145 | it('return true', () => {
146 | const wrapper = mount();
149 |
150 | assert(_.isBoolean(wrapper.instance().isNextPageAvailable()), 'return boolean');
151 | assert.isTrue(wrapper.instance().isNextPageAvailable(), 'return true');
152 | });
153 | });
154 | });
155 |
156 | describe(': isPrevPageAvailable', () => {
157 | const date = moment('2018-08-12');
158 |
159 | describe('is not available by minDate', () => {
160 | it('return false', () => {
161 | const wrapper = mount();
164 |
165 | assert(_.isBoolean(wrapper.instance().isPrevPageAvailable()), 'return boolean');
166 | assert.isFalse(wrapper.instance().isPrevPageAvailable(), 'return false');
167 | });
168 | });
169 |
170 | describe('available by minDate', () => {
171 | it('return true', () => {
172 | const wrapper = mount();
175 |
176 | assert(_.isBoolean(wrapper.instance().isPrevPageAvailable()), 'return boolean');
177 | assert.isTrue(wrapper.instance().isPrevPageAvailable(), 'return true');
178 | });
179 | });
180 | });
181 |
182 | describe(': getCurrentDate', () => {
183 | const date = moment('2018-08-12');
184 |
185 | it('return string in format `MMMM DD, YYYY`', () => {
186 | const wrapper = mount();
188 |
189 | assert(_.isString(wrapper.instance().getCurrentDate()), 'return string');
190 | assert.equal(wrapper.instance().getCurrentDate(), date.format('MMMM DD, YYYY'), 'return proper value');
191 | });
192 | });
193 |
194 | describe(': handleChange', () => {
195 | const date = moment('2018-08-12 10:00');
196 |
197 | it('call onChangeFake with { year: number, month: number, date: number, hour: number }', () => {
198 | const onChangeFake = sinon.fake();
199 | const wrapper = mount();
202 | const possibleValues = wrapper.instance().buildCalendarValues();
203 | /*
204 | [
205 | '**:00', '**:05', '**:10', '**:15', '**:20', '**:25',
206 | '**:30', '**:35', '**:40', '**:45', '**:50', '**:55',
207 | ]
208 | */
209 | wrapper.instance().handleChange('click', { value: possibleValues[8]});
210 | const calledWithArgs = onChangeFake.args[0];
211 |
212 | assert(onChangeFake.calledOnce, 'onChangeFake called once');
213 | assert.equal(calledWithArgs[0], 'click', 'correct first argument');
214 | assert.equal(calledWithArgs[1].value.year, 2018, 'correct year');
215 | assert.equal(calledWithArgs[1].value.month, 7, 'correct month');
216 | assert.equal(calledWithArgs[1].value.date, 12, 'correct date');
217 | assert.equal(calledWithArgs[1].value.hour, 10, 'correct hour');
218 | assert.equal(calledWithArgs[1].value.minute, 40, 'correct hour');
219 | });
220 | });
221 |
222 | describe(': switchToNextPage', () => {
223 | const date = moment('2018-08-12');
224 |
225 | it('shift `date` state field one day forward', () => {
226 | const wrapper = mount();
228 |
229 | assert.equal(wrapper.state('date').date(), 12, 'date not changed yet');
230 | wrapper.instance().switchToNextPage();
231 | assert.equal(wrapper.state('date').date(), 12 + 1, 'date shifted one day forward');
232 | });
233 | });
234 |
235 | describe(': switchToPrevPage', () => {
236 | const date = moment('2018-08-12');
237 |
238 | it('shift `date` state field one day backward', () => {
239 | const wrapper = mount();
241 |
242 | assert.equal(wrapper.state('date').date(), 12, 'date not changed yet');
243 | wrapper.instance().switchToPrevPage();
244 | assert.equal(wrapper.state('date').date(), 12 - 1, 'date shifted one day backward');
245 | });
246 | });
247 |
248 | describe(': getSelectableCellPositions', () => {
249 | const date = moment('2018-08-12 10:00');
250 |
251 | it('return minutes positions that are >= `minDate`', () => {
252 | const wrapper = mount();
255 | const expected = [ 3, 4, 5, 6, 7, 8, 9, 10, 11 ];
256 | const actual = wrapper.instance().getSelectableCellPositions();
257 |
258 | assert.equal(actual.length, expected.length);
259 | expected.forEach((expectPos, i) => {
260 | assert.equal(expectPos, actual[i]);
261 | });
262 | });
263 | });
264 |
--------------------------------------------------------------------------------