sanity-plugin-recurring-dates 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +186 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.esm.js +9173 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +9197 -0
- package/dist/index.js.map +1 -0
- package/package.json +93 -0
- package/sanity.json +8 -0
- package/src/components/CustomRule/CustomRule.tsx +230 -0
- package/src/components/CustomRule/Monthly.tsx +67 -0
- package/src/components/CustomRule/Weekly.tsx +62 -0
- package/src/components/CustomRule/index.tsx +1 -0
- package/src/components/DateInputs/CommonDateTimeInput.tsx +111 -0
- package/src/components/DateInputs/DateInput.tsx +81 -0
- package/src/components/DateInputs/DateTimeInput.tsx +122 -0
- package/src/components/DateInputs/base/DatePicker.tsx +34 -0
- package/src/components/DateInputs/base/DateTimeInput.tsx +104 -0
- package/src/components/DateInputs/base/LazyTextInput.tsx +77 -0
- package/src/components/DateInputs/base/calendar/Calendar.tsx +381 -0
- package/src/components/DateInputs/base/calendar/CalendarDay.tsx +47 -0
- package/src/components/DateInputs/base/calendar/CalendarMonth.tsx +52 -0
- package/src/components/DateInputs/base/calendar/YearInput.tsx +22 -0
- package/src/components/DateInputs/base/calendar/constants.ts +33 -0
- package/src/components/DateInputs/base/calendar/features.ts +4 -0
- package/src/components/DateInputs/base/calendar/utils.ts +34 -0
- package/src/components/DateInputs/index.ts +2 -0
- package/src/components/DateInputs/types.ts +4 -0
- package/src/components/DateInputs/utils.ts +4 -0
- package/src/components/RecurringDate.tsx +139 -0
- package/src/constants.ts +36 -0
- package/src/index.ts +2 -0
- package/src/plugin.tsx +19 -0
- package/src/schema/recurringDates.tsx +44 -0
- package/src/types.ts +27 -0
- package/src/utils.ts +16 -0
- package/v2-incompatible.js +11 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {format, parse} from '@sanity/util/legacyDateFormat'
|
|
2
|
+
import {getMinutes, parseISO, setMinutes} from 'date-fns'
|
|
3
|
+
import React, {useCallback} from 'react'
|
|
4
|
+
|
|
5
|
+
import {CommonDateTimeInput} from './CommonDateTimeInput'
|
|
6
|
+
import {ParseResult} from './types'
|
|
7
|
+
import {isValidDate} from './utils'
|
|
8
|
+
|
|
9
|
+
interface ParsedOptions {
|
|
10
|
+
dateFormat: string
|
|
11
|
+
timeFormat: string
|
|
12
|
+
timeStep: number
|
|
13
|
+
calendarTodayLabel: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface SchemaOptions {
|
|
17
|
+
dateFormat?: string
|
|
18
|
+
timeFormat?: string
|
|
19
|
+
timeStep?: number
|
|
20
|
+
calendarTodayLabel?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type DateTimeInputProps = {
|
|
24
|
+
id: string
|
|
25
|
+
onChange: (date: string | null) => void
|
|
26
|
+
disabled?: boolean
|
|
27
|
+
value: number | Date | null
|
|
28
|
+
type: {
|
|
29
|
+
name: string
|
|
30
|
+
title: string
|
|
31
|
+
description?: string
|
|
32
|
+
options?: SchemaOptions
|
|
33
|
+
placeholder?: string
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD'
|
|
38
|
+
const DEFAULT_TIME_FORMAT = 'HH:mm'
|
|
39
|
+
|
|
40
|
+
function parseOptions(options: SchemaOptions = {}): ParsedOptions {
|
|
41
|
+
return {
|
|
42
|
+
dateFormat: options.dateFormat || DEFAULT_DATE_FORMAT,
|
|
43
|
+
timeFormat: options.timeFormat || DEFAULT_TIME_FORMAT,
|
|
44
|
+
timeStep: ('timeStep' in options && Number(options.timeStep)) || 1,
|
|
45
|
+
calendarTodayLabel: options.calendarTodayLabel || 'Today',
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function serialize(date: Date) {
|
|
50
|
+
return date.toISOString()
|
|
51
|
+
}
|
|
52
|
+
function deserialize(isoString: string): ParseResult {
|
|
53
|
+
const deserialized = new Date(isoString)
|
|
54
|
+
if (isValidDate(deserialized)) {
|
|
55
|
+
return {isValid: true, date: deserialized}
|
|
56
|
+
}
|
|
57
|
+
return {isValid: false, error: `Invalid date value: "${isoString}"`}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// enforceTimeStep takes a dateString and datetime schema options and enforces the time
|
|
61
|
+
// to be within the configured timeStep
|
|
62
|
+
function enforceTimeStep(dateString: string, timeStep: number) {
|
|
63
|
+
if (!timeStep || timeStep === 1) {
|
|
64
|
+
return dateString
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const date = parseISO(dateString)
|
|
68
|
+
const minutes = getMinutes(date)
|
|
69
|
+
const leftOver = minutes % timeStep
|
|
70
|
+
if (leftOver !== 0) {
|
|
71
|
+
return serialize(setMinutes(date, minutes - leftOver))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return serialize(date)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @hidden
|
|
79
|
+
* @beta */
|
|
80
|
+
export function DateTimeInput(props: DateTimeInputProps) {
|
|
81
|
+
const {id, onChange, type, value, disabled, ...rest} = props
|
|
82
|
+
|
|
83
|
+
const {dateFormat, timeFormat, timeStep} = parseOptions(type.options)
|
|
84
|
+
|
|
85
|
+
const handleChange = useCallback(
|
|
86
|
+
(nextDate: string | null) => {
|
|
87
|
+
let date = nextDate
|
|
88
|
+
if (date !== null && timeStep > 1) {
|
|
89
|
+
date = enforceTimeStep(date, timeStep)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
onChange(date)
|
|
93
|
+
},
|
|
94
|
+
[onChange, timeStep],
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
const formatInputValue = React.useCallback(
|
|
98
|
+
(date: Date) => format(date, `${dateFormat} ${timeFormat}`),
|
|
99
|
+
[dateFormat, timeFormat],
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
const parseInputValue = React.useCallback(
|
|
103
|
+
(inputValue: string) => parse(inputValue, `${dateFormat} ${timeFormat}`),
|
|
104
|
+
[dateFormat, timeFormat],
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<CommonDateTimeInput
|
|
109
|
+
id={id}
|
|
110
|
+
{...rest}
|
|
111
|
+
onChange={handleChange}
|
|
112
|
+
deserialize={deserialize}
|
|
113
|
+
formatInputValue={formatInputValue}
|
|
114
|
+
parseInputValue={parseInputValue}
|
|
115
|
+
selectTime
|
|
116
|
+
serialize={serialize}
|
|
117
|
+
timeStep={timeStep}
|
|
118
|
+
value={value}
|
|
119
|
+
readOnly={disabled}
|
|
120
|
+
/>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {Calendar} from './calendar/Calendar'
|
|
3
|
+
|
|
4
|
+
export const DatePicker = React.forwardRef(function DatePicker(
|
|
5
|
+
props: Omit<React.ComponentProps<'div'>, 'onChange'> & {
|
|
6
|
+
value?: Date
|
|
7
|
+
onChange: (nextDate: Date) => void
|
|
8
|
+
selectTime?: boolean
|
|
9
|
+
timeStep?: number
|
|
10
|
+
},
|
|
11
|
+
ref: React.ForwardedRef<HTMLDivElement>
|
|
12
|
+
) {
|
|
13
|
+
const {value = new Date(), onChange, ...rest} = props
|
|
14
|
+
const [focusedDate, setFocusedDay] = React.useState<Date>()
|
|
15
|
+
|
|
16
|
+
const handleSelect = React.useCallback(
|
|
17
|
+
(nextDate: any) => {
|
|
18
|
+
onChange(nextDate)
|
|
19
|
+
setFocusedDay(undefined)
|
|
20
|
+
},
|
|
21
|
+
[onChange]
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Calendar
|
|
26
|
+
{...rest}
|
|
27
|
+
ref={ref}
|
|
28
|
+
selectedDate={value}
|
|
29
|
+
onSelect={handleSelect}
|
|
30
|
+
focusedDate={focusedDate || value}
|
|
31
|
+
onFocusedDateChange={setFocusedDay}
|
|
32
|
+
/>
|
|
33
|
+
)
|
|
34
|
+
})
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React, {forwardRef, useCallback, useRef, useState} from 'react'
|
|
2
|
+
import FocusLock from 'react-focus-lock'
|
|
3
|
+
import {Box, Button, LayerProvider, Popover, useClickOutside, useForwardedRef} from '@sanity/ui'
|
|
4
|
+
import {CalendarIcon} from '@sanity/icons'
|
|
5
|
+
import {DatePicker} from './DatePicker'
|
|
6
|
+
import {LazyTextInput} from './LazyTextInput'
|
|
7
|
+
|
|
8
|
+
export interface DateTimeInputProps {
|
|
9
|
+
customValidity?: string
|
|
10
|
+
id?: string
|
|
11
|
+
inputValue?: string
|
|
12
|
+
onChange: (date: Date | null) => void
|
|
13
|
+
onInputChange?: (event: React.FocusEvent<HTMLInputElement>) => void
|
|
14
|
+
placeholder?: string
|
|
15
|
+
readOnly?: boolean
|
|
16
|
+
selectTime?: boolean
|
|
17
|
+
timeStep?: number
|
|
18
|
+
value?: Date
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DateTimeInput = forwardRef(function DateTimeInput(
|
|
22
|
+
props: DateTimeInputProps,
|
|
23
|
+
ref: React.ForwardedRef<HTMLInputElement>
|
|
24
|
+
) {
|
|
25
|
+
const {value, inputValue, onInputChange, onChange, selectTime, timeStep, ...rest} = props
|
|
26
|
+
const [popoverRef, setPopoverRef] = useState<HTMLElement | null>(null)
|
|
27
|
+
const forwardedRef = useForwardedRef(ref)
|
|
28
|
+
const buttonRef = useRef(null)
|
|
29
|
+
|
|
30
|
+
const [isPickerOpen, setPickerOpen] = useState(false)
|
|
31
|
+
|
|
32
|
+
useClickOutside(() => setPickerOpen(false), [popoverRef])
|
|
33
|
+
|
|
34
|
+
const handleDeactivation = useCallback(() => {
|
|
35
|
+
forwardedRef.current?.focus()
|
|
36
|
+
forwardedRef.current?.select()
|
|
37
|
+
}, [forwardedRef])
|
|
38
|
+
|
|
39
|
+
const handleKeyUp = useCallback((e: any) => {
|
|
40
|
+
if (e.key === 'Escape') {
|
|
41
|
+
setPickerOpen(false)
|
|
42
|
+
}
|
|
43
|
+
}, [])
|
|
44
|
+
|
|
45
|
+
const handleClick = useCallback(() => setPickerOpen(true), [])
|
|
46
|
+
|
|
47
|
+
const suffix = (
|
|
48
|
+
<Box padding={1}>
|
|
49
|
+
<Button
|
|
50
|
+
ref={buttonRef}
|
|
51
|
+
icon={CalendarIcon}
|
|
52
|
+
mode="bleed"
|
|
53
|
+
padding={2}
|
|
54
|
+
onClick={handleClick}
|
|
55
|
+
style={{display: 'block'}}
|
|
56
|
+
data-testid="select-date-button"
|
|
57
|
+
/>
|
|
58
|
+
</Box>
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<LazyTextInput
|
|
63
|
+
ref={forwardedRef}
|
|
64
|
+
{...rest}
|
|
65
|
+
value={inputValue}
|
|
66
|
+
onChange={onInputChange}
|
|
67
|
+
suffix={
|
|
68
|
+
isPickerOpen ? (
|
|
69
|
+
// Note: we're conditionally inserting the popover here due to an
|
|
70
|
+
// issue with popovers rendering incorrectly on subsequent renders
|
|
71
|
+
// see https://github.com/sanity-io/design/issues/519
|
|
72
|
+
<LayerProvider zOffset={1000}>
|
|
73
|
+
<Popover
|
|
74
|
+
constrainSize
|
|
75
|
+
data-testid="date-input-dialog"
|
|
76
|
+
portal
|
|
77
|
+
content={
|
|
78
|
+
<Box overflow="auto">
|
|
79
|
+
<FocusLock onDeactivation={handleDeactivation}>
|
|
80
|
+
<DatePicker
|
|
81
|
+
selectTime={selectTime}
|
|
82
|
+
timeStep={timeStep}
|
|
83
|
+
onKeyUp={handleKeyUp}
|
|
84
|
+
value={value}
|
|
85
|
+
onChange={onChange}
|
|
86
|
+
/>
|
|
87
|
+
</FocusLock>
|
|
88
|
+
</Box>
|
|
89
|
+
}
|
|
90
|
+
open
|
|
91
|
+
placement="bottom"
|
|
92
|
+
ref={setPopoverRef}
|
|
93
|
+
radius={2}
|
|
94
|
+
>
|
|
95
|
+
{suffix}
|
|
96
|
+
</Popover>
|
|
97
|
+
</LayerProvider>
|
|
98
|
+
) : (
|
|
99
|
+
suffix
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
/>
|
|
103
|
+
)
|
|
104
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {TextInput} from '@sanity/ui'
|
|
3
|
+
|
|
4
|
+
type TextInputProps = React.ComponentProps<typeof TextInput>
|
|
5
|
+
|
|
6
|
+
// todo: delete this when v0.34 of @sanity/ui is out
|
|
7
|
+
type Workaround = any
|
|
8
|
+
|
|
9
|
+
type Props = Workaround &
|
|
10
|
+
Omit<TextInputProps, 'onChange'> & {
|
|
11
|
+
onChange?: (
|
|
12
|
+
event: React.FocusEvent<HTMLInputElement> | React.ChangeEvent<HTMLInputElement>
|
|
13
|
+
) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A TextInput that only emit onChange when it has to
|
|
18
|
+
* By default it will only emit onChange when: 1) user hits enter or 2) user leaves the
|
|
19
|
+
* field (e.g. onBlur) and the input value at this time is different from the given `value` prop
|
|
20
|
+
*/
|
|
21
|
+
export const LazyTextInput = React.forwardRef(function LazyTextInput(
|
|
22
|
+
{onChange, onBlur, onKeyPress, value, ...rest}: Props,
|
|
23
|
+
forwardedRef: React.ForwardedRef<HTMLInputElement>
|
|
24
|
+
) {
|
|
25
|
+
const [inputValue, setInputValue] = React.useState<string>()
|
|
26
|
+
|
|
27
|
+
const handleChange = React.useCallback((event: any) => {
|
|
28
|
+
setInputValue(event.currentTarget.value)
|
|
29
|
+
}, [])
|
|
30
|
+
|
|
31
|
+
const checkEvent = React.useCallback(
|
|
32
|
+
(event: any) => {
|
|
33
|
+
const currentValue = event.currentTarget.value
|
|
34
|
+
if (currentValue !== `${value}`) {
|
|
35
|
+
if (onChange) {
|
|
36
|
+
onChange(event)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
setInputValue(undefined)
|
|
40
|
+
},
|
|
41
|
+
[onChange, value]
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const handleBlur = React.useCallback(
|
|
45
|
+
(e: any) => {
|
|
46
|
+
checkEvent(e)
|
|
47
|
+
if (onBlur) {
|
|
48
|
+
onBlur(e)
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
[checkEvent, onBlur]
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
const handleKeyPress = React.useCallback(
|
|
55
|
+
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
56
|
+
if (e.key === 'Enter') {
|
|
57
|
+
checkEvent(e)
|
|
58
|
+
}
|
|
59
|
+
if (onKeyPress) {
|
|
60
|
+
onKeyPress(e)
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
[checkEvent, onKeyPress]
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<TextInput
|
|
68
|
+
{...rest}
|
|
69
|
+
data-testid="date-input"
|
|
70
|
+
ref={forwardedRef}
|
|
71
|
+
value={inputValue === undefined ? value : inputValue}
|
|
72
|
+
onChange={handleChange}
|
|
73
|
+
onBlur={handleBlur}
|
|
74
|
+
onKeyPress={handleKeyPress}
|
|
75
|
+
/>
|
|
76
|
+
)
|
|
77
|
+
})
|