UNPKG

@nateradebaugh/react-datetime

Version:

A lightweight but complete datetime picker React.js component

775 lines (766 loc) 26.3 kB
import React__default, { createElement, Fragment, forwardRef, useMemo, useCallback, useState, useEffect, useRef } from 'react'; import { Popover } from '@reach/popover'; import useOnClickOutside from 'use-onclickoutside'; import format from 'date-fns/format'; import rawParse from 'date-fns/parse'; import isDate from 'date-fns/isDate'; import toDate from 'date-fns/toDate'; import isDateValid from 'date-fns/isValid'; import startOfDay from 'date-fns/startOfDay'; import clsx from 'clsx'; import getHours from 'date-fns/getHours'; import addHours from 'date-fns/addHours'; import addMinutes from 'date-fns/addMinutes'; import addSeconds from 'date-fns/addSeconds'; import addMilliseconds from 'date-fns/addMilliseconds'; import setHours from 'date-fns/setHours'; import addDays from 'date-fns/addDays'; import differenceInDays from 'date-fns/differenceInDays'; import startOfWeek from 'date-fns/startOfWeek'; import startOfMonth from 'date-fns/startOfMonth'; import endOfMonth from 'date-fns/endOfMonth'; import isSameDay from 'date-fns/isSameDay'; import isBefore from 'date-fns/isBefore'; import addMonths from 'date-fns/addMonths'; import getDate from 'date-fns/getDate'; import addYears from 'date-fns/addYears'; import isSameMonth from 'date-fns/isSameMonth'; import setMonth from 'date-fns/setMonth'; import getDaysInMonth from 'date-fns/getDaysInMonth'; import setDate from 'date-fns/setDate'; import getYear from 'date-fns/getYear'; import setYear from 'date-fns/setYear'; import getDaysInYear from 'date-fns/getDaysInYear'; import setDayOfYear from 'date-fns/setDayOfYear'; import isSameYear from 'date-fns/isSameYear'; function _extends() { _extends = Object.assign ? Object.assign.bind() : 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); } function _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; } var allCounters = ["hours", "minutes", "seconds", "milliseconds"]; var defaultTimeConstraints = { hours: { step: 1 }, minutes: { step: 1 }, seconds: { step: 1 }, milliseconds: { step: 1 } }; var TimePart = function TimePart(props) { var showPrefix = props.showPrefix, onUp = props.onUp, onDown = props.onDown, value = props.value; return value !== null && value !== undefined ? createElement(Fragment, null, showPrefix && createElement("div", { className: "rdtCounterSeparator" }, ":"), createElement("div", { className: "rdtCounter" }, createElement("span", { className: "rdtBtn", onMouseDown: onUp }, "\u25B2"), createElement("div", { className: "rdtCount" }, value), createElement("span", { className: "rdtBtn", onMouseDown: onDown }, "\u25BC"))) : null; }; function getStepSize(type, timeConstraints) { var step = defaultTimeConstraints[type].step; var config = timeConstraints ? timeConstraints[type] : undefined; if (config && config.step) { step = config.step; } return step; } var changeLookup = { hours: addHours, minutes: addMinutes, seconds: addSeconds, milliseconds: addMilliseconds }; function change(op, type, timestamp, timeConstraints) { var mult = op === "sub" ? -1 : 1; var step = getStepSize(type, timeConstraints) * mult; return changeLookup[type](timestamp, step); } function getFormatted(type, timestamp, timeFormat, formatOptions) { var fmt = timeFormat; function has(f, val) { return f.indexOf(val) !== -1; } var hasHours = has(fmt.toLowerCase(), FORMATS.SHORT_HOUR); var hasMinutes = has(fmt, FORMATS.SHORT_MINUTE); var hasSeconds = has(fmt, FORMATS.SHORT_SECOND); var hasMilliseconds = has(fmt, FORMATS.SHORT_MILLISECOND); var hasDayPart = has(fmt, FORMATS.AM_PM); var typeFormat = type === "hours" && hasHours ? hasDayPart ? FORMATS.HOUR : FORMATS.MILITARY_HOUR : type === "minutes" && hasMinutes ? FORMATS.MINUTE : type === "seconds" && hasSeconds ? FORMATS.SECOND : type === "milliseconds" && hasMilliseconds ? FORMATS.MILLISECOND : type === "daypart" && hasDayPart ? FORMATS.AM_PM : undefined; if (typeFormat) { return format(timestamp, typeFormat, formatOptions); } return undefined; } function toggleDayPart(timestamp, setSelectedDate) { return function () { var hours = getHours(timestamp); var newHours = hours >= 12 ? hours - 12 : hours + 12; setSelectedDate(setHours(timestamp, newHours)); }; } var timer; var increaseTimer; var _mouseUpListener; function onStartClicking(op, type, props) { return function () { var readonly = props.readonly, origViewTimestamp = props.viewTimestamp, timeConstraints = props.timeConstraints, setViewTimestamp = props.setViewTimestamp, setSelectedDate = props.setSelectedDate; if (!readonly) { var viewTimestamp = change(op, type, origViewTimestamp, timeConstraints); setViewTimestamp(viewTimestamp); timer = setTimeout(function () { increaseTimer = setInterval(function () { viewTimestamp = change(op, type, viewTimestamp, timeConstraints); setViewTimestamp(viewTimestamp); }, 70); }, 500); _mouseUpListener = function mouseUpListener() { clearTimeout(timer); clearInterval(increaseTimer); setSelectedDate(viewTimestamp); document.body.removeEventListener("mouseup", _mouseUpListener); document.body.removeEventListener("touchend", _mouseUpListener); }; document.body.addEventListener("mouseup", _mouseUpListener); document.body.addEventListener("touchend", _mouseUpListener); } }; } function TimeView(props) { var viewTimestamp = props.viewTimestamp, dateFormat = props.dateFormat, setViewMode = props.setViewMode, timeFormat = props.timeFormat, formatOptions = props.formatOptions, setSelectedDate = props.setSelectedDate; var numCounters = 0; return createElement("div", { className: "rdtTime", "data-testid": "time-picker" }, createElement("table", null, dateFormat ? createElement("thead", null, createElement("tr", null, createElement("th", { className: "rdtSwitch", "data-testid": "time-mode-switcher", colSpan: 4, onClick: function onClick() { return setViewMode("days"); } }, format(viewTimestamp, dateFormat)))) : null, createElement("tbody", null, createElement("tr", null, createElement("td", null, createElement("div", { className: "rdtCounters" }, allCounters.map(function (type) { var val = getFormatted(type, viewTimestamp, timeFormat, formatOptions); if (val) { numCounters++; } return createElement(TimePart, { key: type, showPrefix: numCounters > 1, onUp: onStartClicking("add", type, props), onDown: onStartClicking("sub", type, props), value: val }); }), createElement(TimePart, { onUp: toggleDayPart(viewTimestamp, setSelectedDate), onDown: toggleDayPart(viewTimestamp, setSelectedDate), value: getFormatted("daypart", viewTimestamp, timeFormat, formatOptions) }))))))); } function DaysView(props) { var timeFormat = props.timeFormat, viewDate = props.viewDate, setViewDate = props.setViewDate, selectedDate = props.selectedDate, setSelectedDate = props.setSelectedDate, formatOptions = props.formatOptions, setViewMode = props.setViewMode, isValidDate = props.isValidDate; var weekStart = startOfWeek(viewDate, formatOptions); var prevMonth = addMonths(viewDate, -1); var daysSincePrevMonthLastWeekStart = differenceInDays(startOfWeek(endOfMonth(prevMonth), formatOptions), viewDate); var prevMonthLastWeekStart = addDays(viewDate, daysSincePrevMonthLastWeekStart); return createElement("div", { className: "rdtDays", "data-testid": "day-picker" }, createElement("table", null, createElement("thead", null, createElement("tr", null, createElement("th", { className: "rdtPrev", onClick: function onClick() { return setViewDate(addMonths(viewDate, -1)); } }, createElement("span", null, "\u2039")), createElement("th", { className: "rdtSwitch", "data-testid": "day-mode-switcher", onClick: function onClick() { return setViewMode("months"); }, colSpan: 5 }, format(viewDate, FORMATS.FULL_MONTH_NAME + " " + FORMATS.YEAR, formatOptions)), createElement("th", { className: "rdtNext", onClick: function onClick() { return setViewDate(addMonths(viewDate, 1)); } }, createElement("span", null, "\u203A"))), createElement("tr", null, [0, 1, 2, 3, 4, 5, 6].map(function (colNum) { return createElement("th", { key: colNum, className: "dow" }, format(addDays(weekStart, colNum), FORMATS.SHORT_DAY_OF_WEEK, formatOptions)); }))), createElement("tbody", null, [0, 1, 2, 3, 4, 5].map(function (rowNum) { // Use 7 columns per row var rowStartDay = rowNum * 7; return createElement("tr", { key: format(addDays(prevMonthLastWeekStart, rowStartDay), FORMATS.FULL_TIMESTAMP) }, [0, 1, 2, 3, 4, 5, 6].map(function (d) { var i = d + rowStartDay; var workingDate = addDays(prevMonthLastWeekStart, i); var isDisabled = typeof isValidDate === "function" && !isValidDate(workingDate); var isActive = selectedDate && isSameDay(workingDate, selectedDate); return createElement("td", { key: getDate(workingDate), className: clsx(["rdtDay", { rdtOld: isBefore(workingDate, startOfMonth(viewDate)), rdtNew: isBefore(endOfMonth(viewDate), workingDate), rdtActive: isActive, rdtToday: isSameDay(workingDate, new Date()), rdtDisabled: isDisabled }]), onClick: function onClick() { if (!isDisabled) { setSelectedDate(workingDate); } } }, format(workingDate, FORMATS.SHORT_DAY, formatOptions)); })); })), timeFormat ? createElement("tfoot", null, createElement("tr", null, createElement("td", { onClick: function onClick() { return setViewMode("time"); }, colSpan: 7, className: "rdtTimeToggle", "data-testid": "day-to-time-mode-switcher" }, format(viewDate, timeFormat, formatOptions)))) : null)); } function MonthsView(props) { var viewDate = props.viewDate, setViewDate = props.setViewDate, selectedDate = props.selectedDate, setSelectedDate = props.setSelectedDate, formatOptions = props.formatOptions, setViewMode = props.setViewMode, isValidDate = props.isValidDate; return createElement("div", { className: "rdtMonths", "data-testid": "month-picker" }, createElement("table", null, createElement("thead", null, createElement("tr", null, createElement("th", { className: "rdtPrev", onClick: function onClick() { return setViewDate(addYears(viewDate, -1)); } }, createElement("span", null, "\u2039")), createElement("th", { className: "rdtSwitch", "data-testid": "month-mode-switcher", onClick: function onClick() { return setViewMode("years"); }, colSpan: 2 }, format(viewDate, FORMATS.YEAR, formatOptions)), createElement("th", { className: "rdtNext", onClick: function onClick() { return setViewDate(addYears(viewDate, 1)); } }, createElement("span", null, "\u203A"))))), createElement("table", null, createElement("tbody", null, [0, 1, 2].map(function (rowNum) { // Use 4 columns per row var rowStartMonth = rowNum * 4; return createElement("tr", { key: rowStartMonth }, [0, 1, 2, 3].map(function (m) { var month = m + rowStartMonth; var currentMonth = setMonth(viewDate, month); var daysInMonths = Array.from({ length: getDaysInMonth(currentMonth) }, function (e, i) { return setDate(currentMonth, i + 1); }); var isDisabled = daysInMonths.every(function (d) { return typeof isValidDate === "function" && !isValidDate(d); }); var monthDate = setMonth(new Date(), month); var isActive = selectedDate && isSameMonth(selectedDate, currentMonth); return createElement("td", { key: month, className: clsx(["rdtMonth", { rdtDisabled: isDisabled, rdtActive: isActive }]), onClick: function onClick() { if (!isDisabled) { setSelectedDate(setMonth(viewDate, month)); } } }, format(monthDate, FORMATS.SHORT_MONTH_NAME, formatOptions)); })); })))); } function YearsView(props) { var viewDate = props.viewDate, setViewDate = props.setViewDate, selectedDate = props.selectedDate, setSelectedDate = props.setSelectedDate, formatOptions = props.formatOptions, setViewMode = props.setViewMode, isValidDate = props.isValidDate; var startYear = Math.floor(getYear(viewDate) / 10) * 10; return createElement("div", { className: "rdtYears", "data-testid": "year-picker" }, createElement("table", null, createElement("thead", null, createElement("tr", null, createElement("th", { className: "rdtPrev", onClick: function onClick() { return setViewDate(addYears(viewDate, -10)); } }, createElement("span", null, "\u2039")), createElement("th", { className: "rdtSwitch", "data-testid": "year-mode-switcher", onClick: function onClick() { return setViewMode("years"); }, colSpan: 2 }, startYear, "-", startYear + 9), createElement("th", { className: "rdtNext", onClick: function onClick() { return setViewDate(addYears(viewDate, 10)); } }, createElement("span", null, "\u203A"))))), createElement("table", null, createElement("tbody", null, [0, 1, 2].map(function (rowNum) { // Use 4 columns per row var rowStartYear = startYear - 1 + rowNum * 4; return createElement("tr", { key: rowStartYear }, [0, 1, 2, 3].map(function (y) { var year = y + rowStartYear; var currentYear = setYear(viewDate, year); var daysInYear = Array.from({ length: getDaysInYear(viewDate) }, function (e, i) { return setDayOfYear(currentYear, i + 1); }); var isDisabled = daysInYear.every(function (d) { return typeof isValidDate === "function" && !isValidDate(d); }); var isActive = selectedDate && isSameYear(selectedDate, currentYear); return createElement("td", { key: year, className: clsx(["rdtYear", { rdtDisabled: isDisabled, rdtActive: isActive }]), onClick: function onClick() { if (!isDisabled) { setSelectedDate(setYear(viewDate, year)); } } }, format(currentYear, "yyyy", formatOptions)); })); })))); } var _excluded = ["viewMode", "isStatic", "id", "className", "style"]; var viewLookup = { time: TimeView, months: MonthsView, years: YearsView, days: DaysView }; var CalendarContainer = /*#__PURE__*/forwardRef(function CalendarContainer(props, ref) { var viewMode = props.viewMode, isStatic = props.isStatic, id = props.id, className = props.className, style = props.style, rest = _objectWithoutPropertiesLoose(props, _excluded); if (!viewMode) { return null; } var CalendarElement = viewLookup[viewMode]; return createElement("div", { ref: ref, id: id, "data-testid": "picker-wrapper", className: clsx(["rdtPicker", className, { rdtStatic: isStatic }]), style: style }, CalendarElement && createElement(CalendarElement, Object.assign({}, rest))); }); var _excluded$1 = ["isValidDate", "dateTypeMode", "value", "onChange", "onBlur", "onFocus", "dateFormat", "timeFormat", "locale", "weekStartsOn", "shouldHideInput", "timeConstraints"]; var FORMATS = { MONTH: "LL", SHORT_MONTH_NAME: "LLL", FULL_MONTH_NAME: "LLLL", SHORT_DAY: "d", DAY: "dd", SHORT_DAY_OF_WEEK: "iiiiii", YEAR: "yyyy", MILITARY_HOUR: "H", HOUR: "h", SHORT_HOUR: "h", SHORT_MINUTE: "m", MINUTE: "mm", SHORT_SECOND: "s", SECOND: "ss", SHORT_MILLISECOND: "SSS", MILLISECOND: "SSS", AM_PM: "a", FULL_TIMESTAMP: "yyyy-MM-dd'T'HH:mm:ss.SSSxxx" }; function useDefaultStateWithOverride(defaultValue) { var _useState = useState(undefined), override = _useState[0], setOverride = _useState[1]; var value = override || defaultValue; // Clear the override if the default changes useEffect(function () { setOverride(undefined); }, [defaultValue]); return [value, setOverride]; } function useDefaultDateWithOverride(defaultValue) { var _useState2 = useState(undefined), override = _useState2[0], setOverride = _useState2[1]; var value = override || defaultValue; // Clear the override if the default changes var changeVal = defaultValue.getTime(); useEffect(function () { setOverride(undefined); }, [changeVal]); return [value, setOverride]; } function parse(date, fullFormat, formatOptions) { if (typeof date === "string") { var asDate = rawParse(date, fullFormat, new Date(), formatOptions); if (isDateValid(asDate)) { var formatted = format(asDate, fullFormat, formatOptions); if (date === formatted) { return asDate; } } } else if (date) { var _asDate = toDate(date); if (isDateValid(_asDate)) { return _asDate; } } return undefined; } var nextViewModes = { days: "days", months: "days", years: "months" }; function getDefaultViewMode(dateFormat, timeFormat) { if (dateFormat) { if (dateFormat.match(/[d]/)) { return "days"; } else if (dateFormat.indexOf("L") !== -1) { return "months"; } else if (dateFormat.indexOf("y") !== -1) { return "years"; } } if (timeFormat) { return "time"; } return undefined; } function getDateTypeMode(rawDateTypeMode) { if (typeof rawDateTypeMode === "string") { var lowerRawDateTypeMode = rawDateTypeMode.toLowerCase(); switch (lowerRawDateTypeMode) { case "utc-ms-timestamp": case "input-format": return lowerRawDateTypeMode; } } return "Date"; } // Please do not use types off of a default export module or else Storybook Docs will suffer. // see: https://github.com/storybookjs/storybook/issues/9556 var DateTime = function DateTime(props) { var isValidDate = props.isValidDate, rawDateTypeMode = props.dateTypeMode, value = props.value, rawOnChange = props.onChange, onBlur = props.onBlur, onFocus = props.onFocus, _props$dateFormat = props.dateFormat, rawDateFormat = _props$dateFormat === void 0 ? true : _props$dateFormat, _props$timeFormat = props.timeFormat, rawTimeFormat = _props$timeFormat === void 0 ? true : _props$timeFormat, locale = props.locale, weekStartsOn = props.weekStartsOn, _props$shouldHideInpu = props.shouldHideInput, shouldHideInput = _props$shouldHideInpu === void 0 ? false : _props$shouldHideInpu, timeConstraints = props.timeConstraints, rest = _objectWithoutPropertiesLoose(props, _excluded$1); var isDisabled = props.disabled || props.readOnly; // // Formats // var defaultDateFormat = FORMATS.MONTH + "/" + FORMATS.DAY + "/" + FORMATS.YEAR; var dateFormat = rawDateFormat === true ? defaultDateFormat : rawDateFormat === false ? "" : rawDateFormat; var defaultTimeFormat = FORMATS.HOUR + ":" + FORMATS.MINUTE + " " + FORMATS.AM_PM; var timeFormat = rawTimeFormat === true ? defaultTimeFormat : rawTimeFormat === false ? "" : rawTimeFormat; var fullFormat = dateFormat && timeFormat ? dateFormat + " " + timeFormat : dateFormat || timeFormat || ""; var formatOptions = useMemo(function () { return { locale: locale, weekStartsOn: typeof weekStartsOn === "number" ? weekStartsOn % 7 : weekStartsOn }; }, [locale, weekStartsOn]); var valueAsDate = parse(value, fullFormat, formatOptions); var dateTypeMode = getDateTypeMode(rawDateTypeMode); var getChangedValue = useCallback(function (newValue) { if (typeof newValue === "string") { return newValue; } if (!newValue) { return newValue; } switch (dateTypeMode) { case "utc-ms-timestamp": return newValue.getTime(); case "input-format": return format(newValue, fullFormat, formatOptions); } return newValue; }, [dateTypeMode, formatOptions, fullFormat]); // // On Change // string -> string // falsy -> raw onChange // Date -> if numeric, number (ms) // Date -> if not numeric, Date // var onChange = useCallback(function (newValue) { if (typeof rawOnChange !== "function") { return; } var changedValue = getChangedValue(newValue); // // Suppress change event when the value didn't change! // if (value && changedValue && isDate(value) && isDate(changedValue)) { var oldValStr = format(value, fullFormat, formatOptions); var newValStr = format(changedValue, fullFormat, formatOptions); if (oldValStr === newValStr) { return; } } rawOnChange(changedValue); }, [formatOptions, fullFormat, getChangedValue, rawOnChange, value]); // // ViewDate // var _useDefaultDateWithOv = useDefaultDateWithOverride(valueAsDate || startOfDay(new Date())), viewDate = _useDefaultDateWithOv[0], setViewDate = _useDefaultDateWithOv[1]; // // ViewMode // var defaultViewMode = getDefaultViewMode(dateFormat, timeFormat); var _useDefaultStateWithO = useDefaultStateWithOverride(defaultViewMode), viewMode = _useDefaultStateWithO[0], setViewMode = _useDefaultStateWithO[1]; // // ViewTimestamp // var _useDefaultDateWithOv2 = useDefaultDateWithOverride(valueAsDate || viewDate), viewTimestamp = _useDefaultDateWithOv2[0], setViewTimestamp = _useDefaultDateWithOv2[1]; // // IsOpen // var _useState3 = useState(false), isOpen = _useState3[0], setIsOpen = _useState3[1]; function open() { // Don't allow opening if disabled if (isDisabled) { return; } if (!isOpen && viewMode) { setIsOpen(true); if (typeof onFocus === "function") { onFocus(); } } } function closeWith(newValue) { if (isOpen) { setIsOpen(false); if (typeof onBlur === "function") { var changedValue = getChangedValue(newValue); onBlur(changedValue); } } } function close() { return closeWith(valueAsDate); } // // SetSelectedDate // function setSelectedDate(newDate, tryClose) { if (tryClose === void 0) { tryClose = true; } var asDate = toDate(newDate); setViewDate(asDate); setViewTimestamp(asDate); // Time switches value but stays open if (viewMode === "time") { onChange(newDate); } // When view mode is the default, switch and try to close else if (viewMode === defaultViewMode) { onChange(newDate); if (tryClose) { closeWith(newDate); } } // When view mode is not the default, switch to the next view mode else { var newViewMode = viewMode ? nextViewModes[viewMode] : undefined; setViewMode(newViewMode); } } // // Trigger change when important props change // useEffect(function () { if (valueAsDate) { setSelectedDate(valueAsDate); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [dateTypeMode, fullFormat]); function onInputChange(e) { var newValue = e.target.value; var newValueAsDate = parse(newValue, fullFormat, formatOptions); if (newValueAsDate) { setSelectedDate(newValueAsDate, false); } else { onChange(newValue); } } function onInputKeyDown(e) { if (isOpen) { switch (e.code) { // Enter key case "Enter": // Eat enter key e.preventDefault(); if (inputRef.current) { inputRef.current.blur(); } close(); break; // Escape key case "Escape": if (inputRef.current) { inputRef.current.blur(); } close(); break; // Tab key case "Tab": close(); break; } } else { switch (e.code) { // Down arrow case "ArrowDown": open(); break; } } } var inputRef = useRef(null); var contentRef = useRef(null); useOnClickOutside(contentRef, close); var valueStr = valueAsDate && fullFormat ? format(valueAsDate, fullFormat, formatOptions) : typeof value === "string" ? value : ""; // // Input Props // var finalInputProps = _extends({}, rest, { ref: inputRef, type: "text", onClick: open, onFocus: open, onChange: onInputChange, onKeyDown: onInputKeyDown, value: valueStr }); // // Calendar props // var calendarProps = { ref: contentRef, dateFormat: dateFormat, timeFormat: timeFormat, viewDate: viewDate, setViewDate: setViewDate, selectedDate: valueAsDate, setSelectedDate: setSelectedDate, viewTimestamp: viewTimestamp, setViewTimestamp: setViewTimestamp, formatOptions: formatOptions, viewMode: viewMode, setViewMode: setViewMode, isValidDate: isValidDate, isStatic: shouldHideInput, timeConstraints: timeConstraints }; return !shouldHideInput ? React__default.createElement(React__default.Fragment, null, React__default.createElement("input", Object.assign({}, finalInputProps)), isOpen && React__default.createElement(Popover, { targetRef: inputRef }, React__default.createElement(CalendarContainer, Object.assign({}, calendarProps)))) : React__default.createElement(CalendarContainer, Object.assign({}, finalInputProps, calendarProps)); }; export default DateTime; export { DateTime, FORMATS }; //# sourceMappingURL=react-datetime.esm.js.map