UNPKG

15.5 kBJavaScriptView Raw
1const _excluded = ["id", "autoFocus", "bordered", "views", "tabIndex", "disabled", "readOnly", "className", "value", "defaultValue", "onChange", "currentDate", "defaultCurrentDate", "onCurrentDateChange", "min", "max", "view", "defaultView", "onViewChange", "onKeyDown", "onNavigate", "renderDay", "messages", "formats"];
2
3function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
4
5function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
6
7import cn from 'classnames';
8import PropTypes from 'prop-types';
9import React, { useEffect, useRef } from 'react';
10import { useUncontrolledProp } from 'uncontrollable';
11import CalendarHeader from './CalendarHeader';
12import Century from './Century';
13import Decade from './Decade';
14import { useLocalizer } from './Localization';
15import Month from './Month';
16import SlideTransitionGroup from './SlideTransitionGroup';
17import Widget from './Widget';
18import Year from './Year';
19import dates from './dates';
20import useAutoFocus from './useAutoFocus';
21import useFocusManager from './useFocusManager';
22import { notify, useInstanceId } from './WidgetHelpers';
23
24let last = a => a[a.length - 1];
25
26const CELL_CLASSNAME = 'rw-cell';
27const FOCUSED_CELL_SELECTOR = `.${CELL_CLASSNAME}[tabindex]`;
28const MIN = new Date(1900, 0, 1);
29const MAX = new Date(2099, 11, 31);
30const VIEW_OPTIONS = ['month', 'year', 'decade', 'century'];
31const VIEW_UNIT = {
32 month: 'day',
33 year: 'month',
34 decade: 'year',
35 century: 'decade'
36};
37const VIEW = {
38 month: Month,
39 year: Year,
40 decade: Decade,
41 century: Century
42};
43const ARROWS_TO_DIRECTION = {
44 ArrowDown: 'DOWN',
45 ArrowUp: 'UP',
46 ArrowRight: 'RIGHT',
47 ArrowLeft: 'LEFT'
48};
49const OPPOSITE_DIRECTION = {
50 LEFT: 'RIGHT',
51 RIGHT: 'LEFT'
52};
53const MULTIPLIER = {
54 year: 1,
55 decade: 10,
56 century: 100
57};
58
59function inRangeValue(_value, min, max) {
60 let value = dateOrNull(_value);
61 if (value === null) return value;
62 return dates.max(dates.min(value, max), min);
63}
64
65const propTypes = {
66 /**
67 * @example ['disabled', ['new Date()']]
68 */
69 disabled: PropTypes.bool,
70
71 /**
72 * @example ['readOnly', ['new Date()']]
73 */
74 readOnly: PropTypes.bool,
75
76 /**
77 * @example ['onChangePicker', [ ['new Date()'] ]]
78 */
79 onChange: PropTypes.func,
80
81 /**
82 * The selected Date.
83 *
84 * ```tsx live
85 * import { Calendar } from 'react-widgets';
86 *
87 * <Calendar value={new Date()} />
88 * ```
89 * @example false
90 */
91 value: PropTypes.instanceOf(Date),
92
93 /**
94 * The minimum date that the Calendar can navigate from.
95 *
96 * @example ['prop', ['min', 'new Date()']]
97 */
98 min: PropTypes.instanceOf(Date),
99
100 /**
101 * The maximum date that the Calendar can navigate to.
102 *
103 * @example ['prop', ['max', 'new Date()']]
104 */
105 max: PropTypes.instanceOf(Date),
106
107 /**
108 * Default current date at which the calendar opens. If none is provided, opens at today's date or the `value` date (if any).
109 */
110 currentDate: PropTypes.instanceOf(Date),
111
112 /**
113 * Change event Handler that is called when the currentDate is changed. The handler is called with the currentDate object.
114 */
115 onCurrentDateChange: PropTypes.func,
116
117 /** Specify the navigate into the past header icon */
118 navigatePrevIcon: PropTypes.node,
119
120 /** Specify the navigate into the future header icon */
121 navigateNextIcon: PropTypes.node,
122
123 /**
124 * Controls the currently displayed calendar view. Use `defaultView` to set a unique starting view.
125 *
126 * @type {("month"|"year"|"decade"|"century")}
127 * @controllable onViewChange
128 */
129 view(props, ...args) {
130 // @ts-ignore
131 return PropTypes.oneOf(props.views || VIEW_OPTIONS)(props, ...args);
132 },
133
134 /**
135 * Defines a list of views the Calendar can traverse through, starting with the
136 * first in the list to the last.
137 *
138 * @type array<"month"|"year"|"decade"|"century">
139 */
140 views: PropTypes.arrayOf(PropTypes.oneOf(VIEW_OPTIONS)),
141
142 /**
143 * A callback fired when the `view` changes.
144 *
145 * @controllable view
146 */
147 onViewChange: PropTypes.func,
148
149 /**
150 * Callback fired when the Calendar navigates between views, or forward and backwards in time.
151 *
152 * @type function(date: ?Date, direction: string, view: string)
153 */
154 onNavigate: PropTypes.func,
155 culture: PropTypes.string,
156 autoFocus: PropTypes.bool,
157
158 /**
159 * Show or hide the Calendar footer.
160 *
161 * @example ['prop', ['footer', true]]
162 */
163 footer: PropTypes.bool,
164
165 /**
166 * Provide a custom component to render the days of the month. The Component is provided the following props
167 *
168 * - `date`: a `Date` object for the day of the month to render
169 * - `label`: a formatted `string` of the date to render. To adjust the format of the `label` string use the `dateFormat` prop, listed below.
170 */
171 renderDay: PropTypes.func,
172 formats: PropTypes.shape({
173 /**
174 * A formatter for the header button of the month view.
175 *
176 * @example ['dateFormat', ['headerFormat', "{ date: 'medium' }"]]
177 */
178 header: PropTypes.any,
179
180 /**
181 * A formatter for the Calendar footer, formats today's Date as a string.
182 *
183 * @example ['dateFormat', ['footerFormat', "{ date: 'medium' }", "date => 'Today is: ' + formatter(date)"]]
184 */
185 footer: PropTypes.any,
186
187 /**
188 * A formatter calendar days of the week, the default formats each day as a Narrow name: "Mo", "Tu", etc.
189 *
190 * @example ['prop', { day: "day => \n['🎉', 'M', 'T','W','Th', 'F', '🎉'][day.getDay()]" }]
191 */
192 day: PropTypes.any,
193
194 /**
195 * A formatter for day of the month
196 *
197 * @example ['prop', { date: "dt => String(dt.getDate())" }]
198 */
199 date: PropTypes.any,
200
201 /**
202 * A formatter for month name.
203 *
204 * @example ['dateFormat', ['monthFormat', "{ raw: 'MMMM' }", null, { defaultView: '"year"' }]]
205 */
206 month: PropTypes.any,
207
208 /**
209 * A formatter for month name.
210 *
211 * @example ['dateFormat', ['yearFormat', "{ raw: 'yy' }", null, { defaultView: '"decade"' }]]
212 */
213 year: PropTypes.any,
214
215 /**
216 * A formatter for decade, the default formats the first and last year of the decade like: 2000 - 2009.
217 */
218 decade: PropTypes.any,
219
220 /**
221 * A formatter for century, the default formats the first and last year of the century like: 1900 - 1999.
222 */
223 century: PropTypes.any
224 }),
225 messages: PropTypes.shape({
226 moveBack: PropTypes.string,
227 moveForward: PropTypes.string
228 }),
229 onKeyDown: PropTypes.func,
230
231 /** @ignore */
232 tabIndex: PropTypes.any
233};
234
235const useViewState = (views, view = views[0], currentDate) => {
236 const lastView = useRef(view);
237 const lastDate = useRef(currentDate);
238 let slideDirection;
239
240 if (view !== lastView.current) {
241 slideDirection = views.indexOf(lastView.current) > views.indexOf(view) ? 'top' : 'bottom';
242 } else if (lastDate.current !== currentDate) {
243 slideDirection = dates.gt(currentDate, lastDate.current) ? 'left' : 'right';
244 }
245
246 useEffect(() => {
247 lastDate.current = currentDate;
248 lastView.current = view;
249 });
250 return slideDirection;
251};
252
253/**
254 * @public
255 */
256function Calendar(_ref) {
257 let {
258 id,
259 autoFocus,
260 bordered = true,
261 views = VIEW_OPTIONS,
262 tabIndex = 0,
263 disabled,
264 readOnly,
265 className,
266 value,
267 defaultValue,
268 onChange,
269 currentDate: pCurrentDate,
270 defaultCurrentDate,
271 onCurrentDateChange,
272 min = MIN,
273 max = MAX,
274 view,
275 defaultView = views[0],
276 onViewChange,
277 onKeyDown,
278 onNavigate,
279 renderDay,
280 messages,
281 formats
282 } = _ref,
283 elementProps = _objectWithoutPropertiesLoose(_ref, _excluded);
284
285 const [currentValue, handleChange] = useUncontrolledProp(value, defaultValue, onChange);
286 const [currentDate, handleCurrentDateChange] = useUncontrolledProp(pCurrentDate, defaultCurrentDate || currentValue || new Date(), onCurrentDateChange);
287 const [currentView, handleViewChange] = useUncontrolledProp(view, defaultView, onViewChange);
288 const localizer = useLocalizer(messages, formats);
289 const ref = useRef(null);
290 const viewId = useInstanceId(id, '_calendar');
291 const labelId = useInstanceId(id, '_calendar_label');
292 useAutoFocus(!!autoFocus, ref);
293 const slideDirection = useViewState(views, currentView, currentDate);
294 const [, focused] = useFocusManager(ref, {
295 disabled
296 }, {
297 willHandle() {
298 if (tabIndex == -1) return false;
299 }
300
301 });
302 const lastValue = useRef(currentValue);
303 useEffect(() => {
304 const inValue = inRangeValue(currentValue, min, max);
305 const last = lastValue.current;
306 lastValue.current = currentValue;
307 if (!dates.eq(inValue, dateOrNull(last), VIEW_UNIT[currentView])) maybeSetCurrentDate(inValue);
308 });
309 const isDisabled = disabled || readOnly;
310 /**
311 * Handlers
312 */
313
314 const handleViewChangeImpl = () => {
315 navigate('UP');
316 };
317
318 const handleMoveBack = () => {
319 navigate('LEFT');
320 };
321
322 const handleMoveForward = () => {
323 navigate('RIGHT');
324 };
325
326 const handleDateChange = date => {
327 if (views[0] === currentView) {
328 maybeSetCurrentDate(date);
329 notify(handleChange, [date]);
330 focus();
331 return;
332 }
333
334 navigate('DOWN', date);
335 };
336
337 const handleMoveToday = () => {
338 let date = new Date();
339 let firstView = views[0];
340 notify(onChange, [date]);
341
342 if (dates.inRange(date, min, max, firstView)) {
343 focus();
344 maybeSetCurrentDate(date);
345 notify(handleViewChange, [firstView]);
346 }
347 };
348
349 const handleKeyDown = e => {
350 let ctrl = e.ctrlKey || e.metaKey;
351 let key = e.key;
352 let direction = ARROWS_TO_DIRECTION[key];
353 let unit = VIEW_UNIT[currentView];
354
355 if (key === 'Enter') {
356 e.preventDefault();
357 return handleDateChange(currentDate);
358 }
359
360 if (direction) {
361 if (ctrl) {
362 e.preventDefault();
363 navigate(direction);
364 } else {
365 const isRTL = getComputedStyle(e.currentTarget).getPropertyValue('direction') === 'rtl';
366 if (isRTL && direction in OPPOSITE_DIRECTION) direction = OPPOSITE_DIRECTION[direction];
367 let nextDate = Calendar.move(currentDate, min, max, currentView, direction);
368
369 if (!dates.eq(currentDate, nextDate, unit)) {
370 e.preventDefault();
371 if (dates.gt(nextDate, currentDate, currentView)) navigate('RIGHT', nextDate);else if (dates.lt(nextDate, currentDate, currentView)) navigate('LEFT', nextDate);else maybeSetCurrentDate(nextDate);
372 }
373 }
374 }
375
376 notify(onKeyDown, [e]);
377 };
378
379 function navigate(direction, date) {
380 let nextView = currentView;
381 let slideDir = direction === 'LEFT' || direction === 'UP' ? 'right' : 'left';
382 if (direction === 'UP') nextView = views[views.indexOf(currentView) + 1] || nextView;
383 if (direction === 'DOWN') nextView = views[views.indexOf(currentView) - 1] || nextView;
384 if (!date) date = ['LEFT', 'RIGHT'].indexOf(direction) !== -1 ? nextDate(direction) : currentDate;
385
386 if (dates.inRange(date, min, max, nextView)) {
387 notify(onNavigate, [date, slideDir, nextView]); //this.focus()
388
389 maybeSetCurrentDate(date);
390 notify(handleViewChange, [nextView]);
391 }
392 }
393
394 const focus = () => {
395 var _ref$current;
396
397 const node = (_ref$current = ref.current) == null ? void 0 : _ref$current.querySelector(FOCUSED_CELL_SELECTOR);
398 node == null ? void 0 : node.focus();
399 };
400
401 const moveFocus = (node, hadFocus) => {
402 let current = document.activeElement;
403
404 if (hadFocus && (!current || !node.contains(current))) {
405 node.focus();
406 }
407 };
408
409 function maybeSetCurrentDate(date) {
410 let inRangeDate = inRangeValue(date ? new Date(date) : currentDate, min, max);
411 if (date === currentDate || dates.eq(inRangeDate, dateOrNull(currentDate), VIEW_UNIT[currentView])) return;
412 notify(handleCurrentDateChange, [inRangeDate]);
413 }
414
415 function nextDate(direction) {
416 let method = direction === 'LEFT' ? 'subtract' : 'add';
417 let unit = currentView === 'month' ? currentView : 'year';
418 let multi = MULTIPLIER[currentView] || 1;
419 return dates[method](currentDate, 1 * multi, unit);
420 }
421
422 function getHeaderLabel() {
423 switch (currentView) {
424 case 'month':
425 return localizer.formatDate(currentDate, 'header');
426
427 case 'year':
428 return localizer.formatDate(currentDate, 'year');
429
430 case 'decade':
431 return localizer.formatDate(dates.startOf(currentDate, 'decade'), 'decade');
432
433 case 'century':
434 return localizer.formatDate(dates.startOf(currentDate, 'century'), 'century');
435 }
436 }
437
438 let View = VIEW[currentView];
439 let todayNotInRange = !dates.inRange(new Date(), min, max, currentView);
440 let key = currentView + '_' + dates[currentView](currentDate); // let elementProps = Props.pickElementProps(this),
441 // let viewProps = pick(uncontrolledProps, View)
442
443 const prevDisabled = isDisabled || !dates.inRange(nextDate('LEFT'), min, max, currentView);
444 const nextDisabled = isDisabled || !dates.inRange(nextDate('RIGHT'), min, max, currentView);
445 return /*#__PURE__*/React.createElement(Widget, _extends({}, elementProps, {
446 role: "group",
447 ref: ref,
448 focused: focused,
449 disabled: disabled,
450 readOnly: readOnly,
451 tabIndex: tabIndex,
452 className: cn(className, 'rw-calendar', bordered && 'rw-calendar-contained')
453 }), /*#__PURE__*/React.createElement(CalendarHeader, {
454 label: getHeaderLabel(),
455 labelId: labelId,
456 localizer: localizer,
457 upDisabled: isDisabled || currentView === last(views),
458 prevDisabled: prevDisabled,
459 todayDisabled: isDisabled || todayNotInRange,
460 nextDisabled: nextDisabled,
461 onViewChange: handleViewChangeImpl,
462 onMoveLeft: handleMoveBack,
463 onMoveRight: handleMoveForward,
464 onMoveToday: handleMoveToday
465 }), /*#__PURE__*/React.createElement(Calendar.Transition, {
466 direction: slideDirection,
467 onTransitionEnd: moveFocus
468 }, /*#__PURE__*/React.createElement(View, {
469 key: key,
470 min: min,
471 max: max,
472 id: viewId,
473 value: currentValue,
474 localizer: localizer,
475 disabled: isDisabled,
476 focusedItem: currentDate,
477 onChange: handleDateChange,
478 onKeyDown: handleKeyDown,
479 "aria-labelledby": labelId,
480 renderDay: renderDay
481 })));
482}
483
484function dateOrNull(dt) {
485 if (dt && !isNaN(dt.getTime())) return dt;
486 return null;
487}
488
489Calendar.displayName = 'Calendar';
490Calendar.propTypes = propTypes; // Calendar.defaultProps = {
491// min: new Date(1900, 0, 1),
492// max: new Date(2099, 11, 31),
493// views: VIEW_OPTIONS,
494// tabIndex: '0',
495// }
496
497Calendar.Transition = SlideTransitionGroup;
498
499Calendar.move = (date, min, max, view, direction) => {
500 let isMonth = view === 'month';
501 let isUpOrDown = direction === 'UP' || direction === 'DOWN';
502 let rangeUnit = view && VIEW_UNIT[view];
503 let addUnit = isMonth && isUpOrDown ? 'week' : VIEW_UNIT[view];
504 let amount = isMonth || !isUpOrDown ? 1 : 4;
505 let newDate;
506 if (direction === 'UP' || direction === 'LEFT') amount *= -1;
507 newDate = dates.add(date, amount, addUnit);
508 return dates.inRange(newDate, min, max, rangeUnit) ? newDate : date;
509};
510
511export default Calendar;
\No newline at end of file