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.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +186 -0
  3. package/dist/index.d.ts +29 -0
  4. package/dist/index.esm.js +9173 -0
  5. package/dist/index.esm.js.map +1 -0
  6. package/dist/index.js +9197 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +93 -0
  9. package/sanity.json +8 -0
  10. package/src/components/CustomRule/CustomRule.tsx +230 -0
  11. package/src/components/CustomRule/Monthly.tsx +67 -0
  12. package/src/components/CustomRule/Weekly.tsx +62 -0
  13. package/src/components/CustomRule/index.tsx +1 -0
  14. package/src/components/DateInputs/CommonDateTimeInput.tsx +111 -0
  15. package/src/components/DateInputs/DateInput.tsx +81 -0
  16. package/src/components/DateInputs/DateTimeInput.tsx +122 -0
  17. package/src/components/DateInputs/base/DatePicker.tsx +34 -0
  18. package/src/components/DateInputs/base/DateTimeInput.tsx +104 -0
  19. package/src/components/DateInputs/base/LazyTextInput.tsx +77 -0
  20. package/src/components/DateInputs/base/calendar/Calendar.tsx +381 -0
  21. package/src/components/DateInputs/base/calendar/CalendarDay.tsx +47 -0
  22. package/src/components/DateInputs/base/calendar/CalendarMonth.tsx +52 -0
  23. package/src/components/DateInputs/base/calendar/YearInput.tsx +22 -0
  24. package/src/components/DateInputs/base/calendar/constants.ts +33 -0
  25. package/src/components/DateInputs/base/calendar/features.ts +4 -0
  26. package/src/components/DateInputs/base/calendar/utils.ts +34 -0
  27. package/src/components/DateInputs/index.ts +2 -0
  28. package/src/components/DateInputs/types.ts +4 -0
  29. package/src/components/DateInputs/utils.ts +4 -0
  30. package/src/components/RecurringDate.tsx +139 -0
  31. package/src/constants.ts +36 -0
  32. package/src/index.ts +2 -0
  33. package/src/plugin.tsx +19 -0
  34. package/src/schema/recurringDates.tsx +44 -0
  35. package/src/types.ts +27 -0
  36. package/src/utils.ts +16 -0
  37. 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
+ })