Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[form-builder] Refactor DateTimeInput and add DateInput #756

Merged
merged 2 commits into from
Apr 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// @flow
import moment from 'moment'
import type Moment from 'moment'
import DatePicker from 'react-datepicker'
import 'react-datepicker/dist/react-datepicker-cssmodules.css' // eslint-disable-line import/no-unassigned-import
import React from 'react'
import FormField from 'part:@sanity/components/formfields/default'
import TextInput from 'part:@sanity/components/textinputs/default'
import styles from './styles/BaseDateTimeInput.css'
import type {Marker} from '../../typedefs'
import Dialog from 'part:@sanity/components/dialogs/default'
import Button from 'part:@sanity/components/buttons/default'
import CalendarIcon from 'part:@sanity/base/calendar-icon'

type Action = {
name: string
}

type Props = {
value: ?Moment,
markers: Array<Marker>,
dateOnly?: boolean,
dateFormat: string,
timeFormat?: string,
timeStep?: number,
todayLabel: string,
title: ?string,
description: ?string,
placeholder: ?string,
readOnly: ?boolean,
onChange: (?Moment) => void,
level: number
}

const getFormat = (dateFormat, timeFormat) => dateFormat + (timeFormat ? ` ${timeFormat}` : '')

type State = {
inputValue: ?string,
isDialogOpen: boolean
}

export default class BaseDateTimeInput extends React.Component<Props, State> {
_datepicker: ?DatePicker

state = {
inputValue: null,
isDialogOpen: false
}

handleInputChange = (event: SyntheticEvent<HTMLInputElement>) => {
const inputValue = event.currentTarget.value
const {onChange, dateFormat, timeFormat} = this.props
const parsed = moment(inputValue, getFormat(dateFormat, timeFormat), true)
if (parsed.isValid()) {
onChange(parsed)
} else {
this.setState({inputValue: inputValue})
}
}

handleDialogChange = (nextMoment?: Moment) => {
const {onChange} = this.props
onChange(nextMoment)
this.setState({inputValue: null})
this.close()
}

handleBlur = () => {
this.setState({inputValue: null})
}

focus() {
if (this._datepicker) {
this._datepicker.input.focus()
}
}

setDatePicker = (datePicker: ?DatePicker) => {
this._datepicker = datePicker
}

handleKeyDown = (event: SyntheticKeyboardEvent<*>) => {
if (event.key === 'Enter') {
this.open()
}
}
open = () => {
this.setState({
isDialogOpen: true
})
}

close = () => {
this.setState({
isDialogOpen: false
})
}

handleDialogOpen = this.open
handleDialogClose = this.close

handleDialogAction = (action: Action) => {
if (action.name === 'close') {
this.close()
}

if (action.name === 'now') {
this.handleDialogChange(moment())
}
}

render() {
const {
value,
markers,
dateOnly,
dateFormat,
timeFormat,
title,
description,
todayLabel,
readOnly,
timeStep,
level
} = this.props

const {inputValue, isDialogOpen} = this.state

const format = getFormat(dateFormat, timeFormat)
const placeholder = this.props.placeholder || `e.g. ${moment().format(format)}`

const validation = markers.filter(marker => marker.type === 'validation')
const errors = validation.filter(marker => marker.level === 'error')

return (
<FormField markers={markers} label={title} level={level} description={description}>
{readOnly && (
<TextInput
customValidity={errors.length > 0 ? errors[0].item.message : ''}
readOnly
value={value ? value.format(format) : ''}
/>
)}
{!readOnly && (
<div className={errors.length > 0 ? styles.inputWrapperWithError : styles.inputWrapper}>
<DatePicker
onKeyDown={this.handleKeyDown}
disabledKeyboardNavigation
selected={value || undefined}
placeholderText={placeholder}
calendarClassName={styles.datePicker}
popperClassName={styles.hiddenPopper}
className={styles.input}
onChange={this.handleDialogChange}
onChangeRaw={this.handleInputChange}
value={inputValue ? inputValue : value && value.format(format)}
dateFormat={dateFormat}
timeFormat={timeFormat}
timeIntervals={timeStep}
ref={this.setDatePicker}
/>
<Button
color="primary"
className={styles.selectButton}
onClick={this.handleDialogOpen}
icon={CalendarIcon}
kind="simple"
>
Select
</Button>
</div>
)}
{isDialogOpen && (
<Dialog
isOpen={isDialogOpen}
onClose={this.handleDialogClose}
onAction={this.handleDialogAction}
actions={[
{name: 'close', title: 'Close'},
{name: 'now', kind: 'simple', color: 'primary', title: todayLabel, secondary: true}
]}
showCloseButton={false}
>
<div className={dateOnly ? styles.dialogDatePicker : styles.dialogDatePickerWithTime}>
<DatePicker
inline
showMonthDropdown
showYearDropdown
selected={value || undefined}
calendarClassName={styles.datePicker}
popperClassName={styles.popper}
className={styles.input}
onChange={this.handleDialogChange}
value={inputValue ? inputValue : value && value.format(format)}
showTimeSelect={!dateOnly}
dateFormat={dateFormat}
timeFormat={timeFormat}
timeIntervals={timeStep}
dropdownMode="select"
/>
</div>
</Dialog>
)}
</FormField>
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// @flow
import type Moment from 'moment'
import moment from 'moment'
import React from 'react'
import PatchEvent, {set, unset} from '../../PatchEvent'
import type {Marker} from '../../typedefs'
import BaseDateTimeInput from './BaseDateTimeInput'

type ParsedOptions = {
dateFormat: string,
calendarTodayLabel: string
}

type SchemaOptions = {
dateFormat?: string,
calendarTodayLabel?: string
}

// This is the format dates are stored on
const VALUE_FORMAT = 'YYYY-MM-DD'

// default to how they are stored
const DEFAULT_DATE_FORMAT = VALUE_FORMAT

type Props = {
value: string,
markers: Array<Marker>,
type: {
name: string,
title: string,
description: string,
options?: SchemaOptions,
placeholder?: string
},
readOnly: ?boolean,
onChange: PatchEvent => void,
level: number
}

function parseOptions(options: SchemaOptions = {}): ParsedOptions {
return {
dateFormat: options.dateFormat || DEFAULT_DATE_FORMAT,
calendarTodayLabel: options.calendarTodayLabel || 'Today'
}
}

export default class DateInput extends React.Component<Props> {
baseDateTimeInputRef: ?BaseDateTimeInput = null

handleChange = (nextMoment?: Moment) => {
const patch = nextMoment ? set(nextMoment.format(VALUE_FORMAT)) : unset()
this.props.onChange(PatchEvent.from([patch]))
}

focus() {
if (this.baseDateTimeInputRef) {
this.baseDateTimeInputRef.focus()
}
}

setBaseInput = (baseInput: ?BaseDateTimeInput) => {
this.baseDateTimeInputRef = baseInput
}

render() {
const {value, markers, type, readOnly, level} = this.props
const {title, description} = type
const momentValue: ?Moment = value ? moment(value) : null

const options = parseOptions(type.options)

return (
<BaseDateTimeInput
dateOnly
ref={this.setBaseInput}
value={momentValue}
readOnly={readOnly}
level={level}
title={title}
description={description}
placeholder={type.placeholder}
markers={markers}
dateFormat={options.dateFormat}
todayLabel={options.calendarTodayLabel}
onChange={this.handleChange}
/>
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import type Moment from 'moment'
// @flow
import moment from 'moment'
import 'react-datepicker/dist/react-datepicker-cssmodules.css' // eslint-disable-line import/no-unassigned-import
import {uniqueId} from 'lodash'
import React from 'react'
import PatchEvent, {set, unset} from '../../PatchEvent'
import type {Marker} from '../../typedefs'
import BaseDateTimeInput from './BaseDateTimeInput'

type ParsedOptions = {
dateFormat: string,
timeFormat: string,
timeStep: number,
calendarTodayLabel: string
}

type SchemaOptions = {
dateFormat?: string,
timeFormat?: string,
timeStep?: number,
calendarTodayLabel?: string
}

const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD'
const DEFAULT_TIME_FORMAT = 'HH:mm'

type Props = {
value: string,
markers: Array<Marker>,
type: {
name: string,
title: string,
description: string,
options?: SchemaOptions,
placeholder?: string
},
readOnly: ?boolean,
onChange: PatchEvent => void,
level: number
}

function parseOptions(options: SchemaOptions = {}): ParsedOptions {
return {
dateFormat: options.dateFormat || DEFAULT_DATE_FORMAT,
timeFormat: options.timeFormat || DEFAULT_TIME_FORMAT,
timeStep: ('timeStep' in options && Number(options.timeStep)) || 15,
calendarTodayLabel: options.calendarTodayLabel || 'Now'
}
}

export default class DateInput extends React.Component<Props> {
baseDateTimeInputRef: ?BaseDateTimeInput = null

handleChange = (nextMoment?: Moment) => {
const patch = nextMoment ? set(nextMoment.toDate().toJSON()) : unset()
this.props.onChange(PatchEvent.from([patch]))
}

focus() {
if (this.baseDateTimeInputRef) {
this.baseDateTimeInputRef.focus()
}
}

setBaseInput = (baseInput: ?BaseDateTimeInput) => {
this.baseDateTimeInputRef = baseInput
}

render() {
const {value, markers, type, readOnly, level} = this.props
const {title, description} = type
const momentValue: ?Moment = value ? moment(value) : null

const options = parseOptions(type.options)

return (
<BaseDateTimeInput
ref={this.setBaseInput}
value={momentValue}
readOnly={readOnly}
level={level}
title={title}
description={description}
placeholder={type.placeholder}
markers={markers}
dateFormat={options.dateFormat}
timeFormat={options.timeFormat}
timeStep={options.timeStep}
todayLabel={options.calendarTodayLabel}
onChange={this.handleChange}
/>
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export {default as DateTimeInput} from './DateTimeInput'
export {default as DateInput} from './DateInput'
Loading