Skip to content

Instantly share code, notes, and snippets.

@elcodabra
Created June 18, 2020 13:20
Show Gist options
  • Save elcodabra/3984fb03e1b8adb8d1532d6469475cd8 to your computer and use it in GitHub Desktop.
Save elcodabra/3984fb03e1b8adb8d1532d6469475cd8 to your computer and use it in GitHub Desktop.
Custom MUI KeyboardDatePicker with suggestions
import React, { useEffect, useRef, useState } from 'react'
import moment from 'moment'
import InputAdornment from '@material-ui/core/InputAdornment'
import FormControl from '@material-ui/core/FormControl'
import TextField from '@material-ui/core/TextField'
import MenuItem from '@material-ui/core/MenuItem'
import Select from '@material-ui/core/Select'
import Paper from '@material-ui/core/Paper'
import ClickAwayListener from '@material-ui/core/ClickAwayListener'
import MenuList from '@material-ui/core/MenuList'
import ClearIcon from '@material-ui/icons/Clear'
import { KeyboardDateTimePicker, KeyboardDatePicker } from '@material-ui/pickers'
import { IconButton } from '@material-ui/core'
const TIME_TYPES = {
HOUR: 'hours',
DAY: 'days',
}
// TODO: getCorrectWord
const TYPES_LIST = [
{ id: TIME_TYPES.HOUR, name: 'часа' },
{ id: TIME_TYPES.DAY, name: 'дня' },
]
const SUGGESTIONS_LIST = [
{ id: 0, type: TIME_TYPES.HOUR, value: 0, name: 'по текущее время' },
{ id: 1, type: TIME_TYPES.HOUR, value: 1, name: 'за 1 час' },
{ id: 2, type: TIME_TYPES.HOUR, value: 2, name: 'за 2 часа' },
{ id: 3, type: TIME_TYPES.DAY, value: 1, name: 'за 1 день' },
{ id: 4, type: TIME_TYPES.DAY, value: 1, name: 'за 2 дня' },
]
const MyTextField = React.memo((props) => {
console.log('PeriodTypeKeyboardNew=', props)
return (
<TextField
style={{ minWidth: 200 }}
{...props}
/>
)
}, (prevProps, nextProps) => {
return prevProps.value === nextProps.value
&& prevProps.disabled === nextProps.disabled
})
function makeMaskFromFormat(format, numberMaskChar) {
return format.replace(/[a-z]/gi, numberMaskChar);
}
function maskedDateFormatter(format, numberMaskChar, refuse) {
return function (value) {
var mask = makeMaskFromFormat(format, numberMaskChar)
var result = '';
var parsed = value.replace(refuse, '');
if (parsed === '') {
return parsed;
}
var i = 0;
var n = 0;
while (i < mask.length) {
var maskChar = mask[i];
if (maskChar === numberMaskChar && n < parsed.length) {
var parsedChar = parsed[n];
result += parsedChar;
n += 1;
} else {
result += maskChar;
}
i += 1;
}
return result;
};
}
const PeriodTypeKeyboard = ({
mode,
currentDate,
...calendarProps
}) => {
const [input, setInput] = useState(null)
const [date, setDate] = useState(null)
const [open, setOpen] = useState(false)
const [readOnly, setReadOnly] = useState(false)
const [type, setType] = useState(TIME_TYPES.HOUR)
const anchorRef = useRef(null)
const handleChange = (date, value) => {
setInput(value)
setDate(value ? moment().subtract(value, type) : null)
handleClose()
}
const handleSelectDate = (date) => {
setDate(date)
setType(null)
setInput(null)
setReadOnly(false)
handleClose()
}
const handleChangeType = (event) => {
setType(event.target.value)
}
const handleOpen = () => {
setOpen(true)
}
const handleClose = () => {
setOpen(false)
if (anchorRef.current && anchorRef.current.focus) {
anchorRef.current.focus()
}
}
const handleListKeyDown = (event) => {
if (event.key === 'Tab') {
event.preventDefault()
handleClose()
}
}
const handleSelectSuggestion = ({ type, value, name }) => {
setInput(name)
setDate(moment().subtract(value, type))
setType(null)
setReadOnly(true)
handleClose()
}
const handleClearSuggestion = (event) => {
event.stopPropagation()
setType(TIME_TYPES.HOUR)
setReadOnly(false)
setInput(null)
setDate(null)
handleClose()
}
// TODO: memo
const labelFn = (date, invalidLabel) => {
// console.log('invalidLabel=', invalidLabel)
return input || (date ? date.format(calendarProps.format) : '')
}
const formatter = React.useMemo(() => {
if (type) return str => str
return maskedDateFormatter(calendarProps.format, '_', /[^\d]+/gi);
}, [calendarProps.format, type])
useEffect(() => {
console.log("useEffect log:", anchorRef.current);
}, [anchorRef])
const KeyboardPicker = React.useMemo(() => {
return mode === 'DATETIME' ? KeyboardDateTimePicker : KeyboardDatePicker
}, [mode])
return (
<div style={{ position: 'relative' }}>
<KeyboardPicker
// variant="inline" // TODO: ломает anchorRef
inputProps={{
style: {
textAlign: 'center',
// fontSize: 'larger',
paddingLeft: 0,
paddingRight: 0,
}
}}
{...calendarProps}
inputRef={anchorRef}
value={date}
onAccept={handleSelectDate}
onChange={handleChange}
labelFunc={labelFn}
rifmFormatter={formatter}
TextFieldComponent={MyTextField}
DialogProps={{
onExited: handleClose,
}}
KeyboardButtonProps={{
size: 'small',
}}
InputAdornmentProps={{
position: 'start',
onClick: (event) => { event.stopPropagation() }
}}
InputProps={{
readOnly,
onClick: handleOpen,
endAdornment: (
<InputAdornment position="end">
{type ? (
<FormControl>
<Select
displayEmpty
disableUnderline
value={type}
onClick={(e) => {
e.stopPropagation()
setOpen(false)
}}
onChange={handleChangeType}
>
{TYPES_LIST.map(({ id, name }) => (
<MenuItem key={id} value={id}>{name}</MenuItem>
))}
</Select>
</FormControl>
) : (
<IconButton size="small" onClick={handleClearSuggestion}>
<ClearIcon />
</IconButton>
)}
</InputAdornment>
),
}}
/>
{open && (
<Paper style={{ position: 'absolute', zIndex: 1, width: '100%' }}>
<ClickAwayListener onClickAway={handleClose}>
<div>
<MenuList onKeyDown={handleListKeyDown}>
{SUGGESTIONS_LIST.map(item => {
if (item.id === 0 && !currentDate) {
return null
}
return (
<MenuItem
key={item.id}
value={item.id}
onClick={() => handleSelectSuggestion(item)}
>
{item.name}
</MenuItem>
)
})}
</MenuList>
</div>
</ClickAwayListener>
</Paper>
)}
</div>
)
}
export default PeriodTypeKeyboard
import React, { useEffect, useRef, useState } from 'react'
import moment from 'moment'
import InputAdornment from '@material-ui/core/InputAdornment'
import FormControl from '@material-ui/core/FormControl'
import TextField from '@material-ui/core/TextField'
import MenuItem from '@material-ui/core/MenuItem'
import Select from '@material-ui/core/Select'
import EventIcon from '@material-ui/icons/Event'
import Paper from '@material-ui/core/Paper'
import ClickAwayListener from '@material-ui/core/ClickAwayListener'
import MenuList from '@material-ui/core/MenuList'
import ClearIcon from '@material-ui/icons/Clear'
import { KeyboardDateTimePicker, DatePicker, DateTimePicker } from '@material-ui/pickers'
const TIME_TYPES = {
HOUR: 'hours',
DAY: 'days',
}
// TODO: getCorrectWord
const TYPES_LIST = [
{ id: TIME_TYPES.HOUR, name: 'часа' },
{ id: TIME_TYPES.DAY, name: 'дня' },
]
const SUGGESTIONS_LIST = [
{ id: 0, type: TIME_TYPES.HOUR, value: 0, name: 'по текущее время' },
{ id: 1, type: TIME_TYPES.HOUR, value: 1, name: 'за 1 час' },
{ id: 2, type: TIME_TYPES.HOUR, value: 2, name: 'за 2 часа' },
{ id: 3, type: TIME_TYPES.DAY, value: 1, name: 'за 1 день' },
{ id: 4, type: TIME_TYPES.DAY, value: 1, name: 'за 2 дня' },
]
const MyTextField = React.memo((props) => {
console.log('PeriodTypeKeyboard=', props)
return (
<TextField
style={{ minWidth: 200 }}
{...props}
/>
)}, (prevProps, nextProps) => {
return prevProps.value === nextProps.value
&& prevProps.disabled === nextProps.disabled
})
function makeMaskFromFormat(format, numberMaskChar) {
return format.replace(/[a-z]/gi, numberMaskChar);
}
function maskedDateFormatter(format, numberMaskChar, refuse) {
return function (value) {
var mask = makeMaskFromFormat(format, numberMaskChar)
var result = '';
var parsed = value.replace(refuse, '');
if (parsed === '') {
return parsed;
}
var i = 0;
var n = 0;
while (i < mask.length) {
var maskChar = mask[i];
if (maskChar === numberMaskChar && n < parsed.length) {
var parsedChar = parsed[n];
result += parsedChar;
n += 1;
} else {
result += maskChar;
}
i += 1;
}
return result;
};
}
const PeriodTypeKeyboardWithCustomDialog = ({
mode,
currentDate,
...calendarProps
}) => {
const [readOnly, setReadOnly] = useState(false)
const [input, setInput] = useState(null)
// TODO: date from props
const [date, setDate] = useState(null)
const [open, setOpen] = useState(null)
const [type, setType] = useState(TIME_TYPES.HOUR)
const anchorRef = useRef(null)
const handleChange = (date, value) => {
setInput(value)
setDate(value ? moment().subtract(value, type) : null)
handleClose()
}
const handleSelectDate = (date) => {
setDate(date)
setType(null)
setInput(null)
setReadOnly(false)
handleClose()
}
const handleChangeType = (event) => {
setType(event.target.value)
}
const handleOpen = (open) => {
setOpen(open)
}
const handleClose = () => {
setOpen(null)
if (anchorRef.current && anchorRef.current.focus) {
anchorRef.current.focus()
}
}
const handleListKeyDown = (event) => {
if (event.key === 'Tab') {
event.preventDefault()
handleOpen(null)
}
}
const handleSelectExactlyDate = (event) => {
event.stopPropagation()
handleOpen('picker')
}
const handleSelectSuggestion = ({ type, value, name }) => {
setInput(name)
setDate(moment().subtract(value, type))
setType(null)
setReadOnly(true)
handleClose()
}
const handleClearSuggestion = (event) => {
event.stopPropagation()
setType(TIME_TYPES.HOUR)
setReadOnly(false)
setInput(null)
setDate(null)
handleClose()
}
// TODO: memo
const labelFn = (date, invalidLabel) => {
// console.log('invalidLabel=', invalidLabel)
return input || (date ? date.format(calendarProps.format) : '')
}
const formatter = React.useMemo(() => {
if (type) return str => str
return maskedDateFormatter(calendarProps.format, '_', /[^\d]+/gi);
}, [calendarProps.format, type])
useEffect(() => {
console.log("useEffect log:", anchorRef.current);
}, [anchorRef])
const Picker = mode === 'DATETIME' ? DateTimePicker : DatePicker
return (
<div style={{ position: 'relative' }}>
<KeyboardDateTimePicker
inputProps={{
style: {
textAlign: 'center',
// fontSize: 'larger',
paddingLeft: 15,
paddingRight: 15,
}
}}
{...calendarProps}
inputRef={anchorRef}
value={date}
onChange={handleChange}
labelFunc={labelFn}
rifmFormatter={formatter}
TextFieldComponent={MyTextField}
InputAdornmentProps={{
position: 'start',
component: () => (
<EventIcon color="primary" onClick={handleSelectExactlyDate} />
),
}}
InputProps={{
readOnly,
onClick: () => handleOpen(open || 'default'),
endAdornment: type ? (
<InputAdornment position="start">
<FormControl>
<Select
displayEmpty
disableUnderline
value={type}
onClick={(e) => {
e.stopPropagation()
setOpen(false)
}}
onChange={handleChangeType}
>
{TYPES_LIST.map(({ id, name }) => (
<MenuItem key={id} value={id}>{name}</MenuItem>
))}
</Select>
</FormControl>
</InputAdornment>
) : (
<ClearIcon fontSize="small" onClick={handleClearSuggestion} />
),
}}
/>
{open && (
<Paper style={{ position: 'absolute', zIndex: 1, width: '100%' }}>
<ClickAwayListener onClickAway={handleClose}>
<div>
{open === 'picker' ? (
<Picker
autoOk
openTo="date"
{...calendarProps}
variant="static"
value={input}
onChange={handleSelectDate}
/>
) : (
<MenuList onKeyDown={handleListKeyDown}>
{SUGGESTIONS_LIST.map(item => {
if (item.id === 0 && !currentDate) {
return null
}
return (
<MenuItem
key={item.id}
value={item.id}
onClick={() => handleSelectSuggestion(item)}
>
{item.name}
</MenuItem>
)
})}
</MenuList>
)}
</div>
</ClickAwayListener>
</Paper>
)}
</div>
)
}
export default PeriodTypeKeyboardWithCustomDialog
import React, { useRef, useState } from 'react'
import InputAdornment from '@material-ui/core/InputAdornment'
import FormControl from '@material-ui/core/FormControl'
import TextField from '@material-ui/core/TextField'
import MenuItem from '@material-ui/core/MenuItem'
import Select from '@material-ui/core/Select'
import EventIcon from '@material-ui/icons/Event'
import Paper from '@material-ui/core/Paper'
import ClickAwayListener from '@material-ui/core/ClickAwayListener'
import MenuList from '@material-ui/core/MenuList'
import { DatePicker } from '@material-ui/pickers'
const TIME_TYPES = {
HOUR: 'h',
DAY: 'd',
}
const TYPES_LIST = [
{ id: TIME_TYPES.HOUR, name: 'часа' },
{ id: TIME_TYPES.DAY, name: 'дня' },
]
const SUGGESTIONS_LIST = [
{ id: 0, type: TIME_TYPES.HOUR, value: 0, name: 'по текущее время' },
{ id: 1, type: TIME_TYPES.HOUR, value: 1, name: 'за 1 час' },
{ id: 2, type: TIME_TYPES.HOUR, value: 2, name: 'за 2 часа' },
{ id: 3, type: TIME_TYPES.DAY, value: 1, name: 'за 1 день' },
{ id: 4, type: TIME_TYPES.DAY, value: 1, name: 'за 2 дня' },
]
const PeriodTypeTextField = ({
currentDate,
textProps,
...calendarProps
}) => {
const [input, setInput] = useState(currentDate ? SUGGESTIONS_LIST[0].name : '')
const [readOnly, setReadOnly] = useState(currentDate)
const [type, setType] = useState(currentDate ? null : TIME_TYPES.HOUR)
const [open, setOpen] = useState(null)
const anchorRef = useRef(null)
/*
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen)
}
*/
const handleOpen = (type) => {
setOpen(type)
}
const handleClose = (event) => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
setOpen(null)
}
function handleListKeyDown(event) {
if (event.key === 'Tab') {
event.preventDefault()
setOpen(null)
}
}
// return focus to the input when we transitioned from !open -> open
const prevOpen = React.useRef(open)
React.useEffect(() => {
if (prevOpen.current && !open) {
anchorRef.current && anchorRef.current.focus && anchorRef.current.focus()
}
prevOpen.current = open
}, [open])
const handleInputChange = (event) => {
setOpen(null)
setInput(event.target.value)
console.log(`${event.target.value} ${type}`)
}
const handleChangeType = (event) => {
setType(event.target.value)
console.log(`${input} ${event.target.value}`)
}
const handleSelectSuggestion = (event, item) => {
setType(null)
setReadOnly(true)
setInput(item.name)
// handleChange(item.type, item.value)
handleClose(event)
}
const handleSelectExactlyDate = (event) => {
event.stopPropagation()
setOpen('picker')
}
const handleChangeDate = (date) => {
setInput(date.format())
setType(null)
setOpen(null)
}
const handleClearSuggestion = (event) => {
event.stopPropagation()
setType(TIME_TYPES.HOUR)
setReadOnly(false)
setInput('')
if (anchorRef.current && anchorRef.current.focus) {
anchorRef.current.focus()
}
// handleChange(item.type, item.value)
}
return (
<div style={{ position: 'relative' }}>
<TextField
style={{ minWidth: 250 }}
fullWidth
// autoFocus
margin="dense"
// variant="outlined"
// type="number"
// error="numbers"
inputProps={{
style: {
textAlign: 'center',
fontSize: 'larger',
paddingLeft: 15,
paddingRight: 15,
}
}}
InputLabelProps={{
shrink: true,
}}
{...textProps}
inputRef={anchorRef}
aria-controls={open ? 'menu-list-grow' : undefined}
aria-haspopup="true"
value={input}
onChange={handleInputChange}
InputProps={{
readOnly,
onClick: () => handleOpen(open || 'default'),
startAdornment: (
<>
<EventIcon color="primary" onClick={handleSelectExactlyDate} />
{type && (<span style={{ paddingLeft: 15 }}>за</span>)}
</>
),
endAdornment: type ? (
<InputAdornment position="start">
<FormControl>
<Select
displayEmpty
disableUnderline
value={type}
onClick={(e) => {
e.stopPropagation()
setOpen(false)
}}
onChange={handleChangeType}
>
{TYPES_LIST.map(({ id, name }) => (
<MenuItem key={id} value={id}>{name}</MenuItem>
))}
</Select>
</FormControl>
</InputAdornment>
) : (
<span onClick={handleClearSuggestion}>[x]</span>
),
}}
/>
{open && (
<Paper style={{ position: 'absolute', zIndex: 1, width: '100%' }}>
<ClickAwayListener onClickAway={handleClose}>
<div>
{open === 'picker' ? (
<DatePicker
autoOk
openTo="date"
{...calendarProps}
variant="static"
onChange={handleChangeDate}
/>
) : (
<MenuList autoFocusItem={open} id="menu-list-grow" onKeyDown={handleListKeyDown}>
<MenuItem onClick={handleSelectExactlyDate}>
<EventIcon color="primary" style={{ marginRight: 5 }} />
указать точную дату
</MenuItem>
{SUGGESTIONS_LIST.map(item => {
if (item.id === 0 && !currentDate) {
return null
}
return (
<MenuItem
key={item.id}
value={item.id}
onClick={(e) => handleSelectSuggestion(e, item)}
>
{item.name}
</MenuItem>
)
})}
</MenuList>
)}
</div>
</ClickAwayListener>
</Paper>
)}
</div>
)
}
export default PeriodTypeTextField
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment