nitro-web 0.0.145 → 0.0.147
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/client/index.ts +5 -5
- package/components/partials/element/calendar.tsx +63 -40
- package/components/partials/element/filters.tsx +98 -69
- package/components/partials/element/timepicker.tsx +119 -0
- package/components/partials/form/field-color.tsx +27 -19
- package/components/partials/form/field-currency.tsx +108 -102
- package/components/partials/form/field-date.tsx +167 -93
- package/components/partials/form/field.tsx +16 -29
- package/components/partials/styleguide.tsx +94 -40
- package/package.json +3 -4
- package/types/util.d.ts +3 -8
- package/types/util.d.ts.map +1 -1
- package/util.js +9 -24
- package/components/partials/form/field-time.tsx +0 -214
|
@@ -15,128 +15,74 @@ type NumericFormatProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
|
|
15
15
|
prefix?: string;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
type Currencies = { [key: string]: { symbol: string, digits: number } }
|
|
19
|
+
type Cents = string|number|null
|
|
20
|
+
export type FieldCurrencyProps = Omit<NumericFormatProps, 'onChange' | 'value' | 'defaultValue'> & {
|
|
19
21
|
name: string
|
|
20
22
|
/** name is applied if id is not provided */
|
|
21
23
|
id?: string
|
|
22
24
|
/** currency iso, e.g. 'nzd' */
|
|
23
25
|
currency: string
|
|
24
26
|
/** override the default currencies array used to lookup currency symbol and digits, e.g. {nzd: { symbol: '$', digits: 2 }} */
|
|
25
|
-
currencies?:
|
|
27
|
+
currencies?: Currencies,
|
|
26
28
|
/** override the default CLDR country currency format, e.g. '¤#,##0.00' */
|
|
27
29
|
format?: string,
|
|
28
|
-
onChange?: (event: { target: { name: string, value:
|
|
30
|
+
onChange?: (event: { target: { name: string, value: Cents } }) => void
|
|
29
31
|
/** value should be in cents */
|
|
30
|
-
value?:
|
|
31
|
-
defaultValue?:
|
|
32
|
+
value?: Cents
|
|
33
|
+
defaultValue?: Cents // defined to just fix typescript error
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
export function FieldCurrency({ currency='nzd', currencies, format, onChange
|
|
36
|
+
export function FieldCurrency({ currency='nzd', currencies, format, onChange: onChangeProp, ...props }: FieldCurrencyProps) {
|
|
35
37
|
const [dontFix, setDontFix] = useState(false)
|
|
36
|
-
const [
|
|
37
|
-
const [
|
|
38
|
+
const [lastBlurred, setLastBlurred] = useState(0)
|
|
39
|
+
const [settings, setSettings] = useState(() => getCurrencySettings(currency, currencies, format, props.name))
|
|
38
40
|
const [prefixWidth, setPrefixWidth] = useState(0)
|
|
39
|
-
const ref = useRef({
|
|
41
|
+
const ref = useRef({ dontFix }) // was null
|
|
40
42
|
const id = props.id || props.name
|
|
41
|
-
ref.current = {
|
|
43
|
+
ref.current = { dontFix }
|
|
42
44
|
|
|
45
|
+
// Since value and onChange are optional, we need need to create an internal value state
|
|
46
|
+
const [internalValue, setInternalValue] = useState<FieldCurrencyProps['value']>(props.value ?? props.defaultValue)
|
|
47
|
+
const inputValue = useMemo(() => to('dollars', internalValue, settings), [internalValue, settings.key, lastBlurred])
|
|
48
|
+
|
|
49
|
+
// Update the internal value when the value changes outside of the component
|
|
43
50
|
useEffect(() => {
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
setSettings(settings)
|
|
47
|
-
setDollars(toDollars(value, true, settings)) // required latest _settings
|
|
48
|
-
}
|
|
49
|
-
}, [currency])
|
|
51
|
+
if (internalValue !== (props.value ?? props.defaultValue)) setInternalValue(props.value ?? props.defaultValue)
|
|
52
|
+
}, [props.value])
|
|
50
53
|
|
|
54
|
+
// Update the settings if the setting parameters change
|
|
51
55
|
useEffect(() => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
} else {
|
|
56
|
-
setDollars(toDollars(value, true))
|
|
57
|
-
}
|
|
58
|
-
}, [value])
|
|
56
|
+
const _settings = getCurrencySettings(currency, currencies, format, props.name)
|
|
57
|
+
if (settings.key !== _settings.key) setSettings(_settings)
|
|
58
|
+
}, [currency, currencies, format])
|
|
59
59
|
|
|
60
|
+
// Get the prefix content width
|
|
60
61
|
useEffect(() => {
|
|
61
|
-
// Get the prefix content width
|
|
62
62
|
setPrefixWidth(settings.prefix ? getPrefixWidth(settings.prefix, 1) : 0)
|
|
63
63
|
}, [settings.prefix])
|
|
64
64
|
|
|
65
|
-
function
|
|
66
|
-
const maxDecimals =
|
|
65
|
+
function to(type: 'dollars' | 'cents', value?: Cents, settings?: { maxDecimals?: number }) {
|
|
66
|
+
const maxDecimals = settings?.maxDecimals
|
|
67
67
|
const parsed = parseFloat(value + '')
|
|
68
68
|
if (!parsed && parsed !== 0) return null
|
|
69
69
|
if (!maxDecimals) return parsed
|
|
70
|
-
const value2 = Math.round(parsed * Math.pow(10, maxDecimals)) // e.g. 1.23 => 123
|
|
71
|
-
// console.log('toCents', parsed, value2)
|
|
72
|
-
return value2
|
|
73
|
-
}
|
|
74
70
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (!parsed && parsed !== 0) return null
|
|
79
|
-
if (!maxDecimals) return parsed
|
|
80
|
-
const value2 = parsed / Math.pow(10, maxDecimals) // e.g. 1.23 => 123
|
|
81
|
-
// console.log('toDollars', value, value2)
|
|
82
|
-
return toFixed ? value2.toFixed(maxDecimals) : value2
|
|
83
|
-
}
|
|
71
|
+
const value2 = type === 'dollars'
|
|
72
|
+
? parsed / Math.pow(10, maxDecimals)
|
|
73
|
+
: parsed * Math.pow(10, maxDecimals) // e.g. 1.23 => 123
|
|
84
74
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const output: {
|
|
88
|
-
currency: string, // e.g. 'nzd'
|
|
89
|
-
decimalSeparator?: string, // e.g. '.'
|
|
90
|
-
thousandSeparator?: string, // e.g. ','
|
|
91
|
-
minDecimals?: number, // e.g. 2
|
|
92
|
-
maxDecimals?: number, // e.g. 2
|
|
93
|
-
prefix?: string, // e.g. '$'
|
|
94
|
-
suffix?: string // e.g. ''
|
|
95
|
-
} = { currency }
|
|
96
|
-
|
|
97
|
-
let _format = format || defaultFormat
|
|
98
|
-
const _currencies = currencies ?? defaultCurrencies
|
|
99
|
-
const currencyObject = _currencies[currency as keyof typeof _currencies]
|
|
100
|
-
if (!currencyObject && currencies) {
|
|
101
|
-
console.error(
|
|
102
|
-
`The currency field "${props.name}" is using the currency "${currency}" which is not found in your currencies object`
|
|
103
|
-
)
|
|
104
|
-
} else if (!currencyObject && !currencies) {
|
|
105
|
-
console.error(
|
|
106
|
-
`The currency field "${props.name}" is using the currency "${currency}" which is not found in the
|
|
107
|
-
default currencies, please provide a currencies object.`
|
|
108
|
-
)
|
|
109
|
-
}
|
|
110
|
-
const symbol = currencyObject ? currencyObject.symbol : ''
|
|
111
|
-
const digits = currencyObject ? currencyObject.digits : 2
|
|
112
|
-
|
|
113
|
-
// Check for currency symbol (¤) and determine its position
|
|
114
|
-
if (_format.indexOf('¤') !== -1) {
|
|
115
|
-
const position = _format.indexOf('¤') === 0 ? 'prefix' : 'suffix'
|
|
116
|
-
output[position] = symbol
|
|
117
|
-
_format = _format.replace('¤', '')
|
|
118
|
-
}
|
|
75
|
+
// dont fix when the user is typing.
|
|
76
|
+
if (type === 'dollars' && ref.current.dontFix) { setDontFix(false) }
|
|
119
77
|
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
if (typeof digits !== 'undefined') {
|
|
129
|
-
output.minDecimals = digits
|
|
130
|
-
output.maxDecimals = digits
|
|
131
|
-
} else {
|
|
132
|
-
const fractionDigits = _format.split(output.decimalSeparator)[1]
|
|
133
|
-
if (fractionDigits) {
|
|
134
|
-
output.minDecimals = fractionDigits.length
|
|
135
|
-
output.maxDecimals = fractionDigits.length
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return output
|
|
78
|
+
// console.log('to', type, value, value2)
|
|
79
|
+
return type === 'cents' || ref.current.dontFix ? value2 : value2.toFixed(maxDecimals)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function onChange(source: 'event' | 'prop', floatValue?: number) {
|
|
83
|
+
if (source === 'event') setDontFix(true)
|
|
84
|
+
if (onChangeProp) onChangeProp({ target: { name: props.name, value: to('cents', floatValue, settings) }})
|
|
85
|
+
else setInternalValue(to('cents', floatValue, settings))
|
|
140
86
|
}
|
|
141
87
|
|
|
142
88
|
return (
|
|
@@ -148,20 +94,15 @@ export function FieldCurrency({ currency='nzd', currencies, format, onChange, va
|
|
|
148
94
|
decimalSeparator={settings.decimalSeparator}
|
|
149
95
|
thousandSeparator={settings.thousandSeparator}
|
|
150
96
|
decimalScale={settings.maxDecimals}
|
|
151
|
-
onValueChange={
|
|
152
|
-
|
|
153
|
-
if (e.source === 'event') setDontFix(true)
|
|
154
|
-
onChange({ target: { name: props.name, value: toCents(floatValue) }})
|
|
155
|
-
}}
|
|
156
|
-
onBlur={() => { setDollars(toDollars(value, true))}}
|
|
97
|
+
onValueChange={({ floatValue }, e) => onChange(e.source, floatValue)}
|
|
98
|
+
onBlur={() => { setLastBlurred(Date.now())}}
|
|
157
99
|
placeholder={props.placeholder || '0.00'}
|
|
158
|
-
value={
|
|
100
|
+
value={inputValue}
|
|
159
101
|
style={{ textIndent: `${prefixWidth}px` }}
|
|
160
102
|
type="text"
|
|
161
|
-
defaultValue={defaultValue}
|
|
162
103
|
/>
|
|
163
104
|
<span
|
|
164
|
-
class={`absolute top-0 bottom-0 left-[12px] left-input-x inline-flex items-center select-none text-gray-500 text-input-base ${
|
|
105
|
+
class={`absolute top-0 bottom-0 left-[12px] left-input-x inline-flex items-center select-none text-gray-500 text-input-base ${inputValue !== null && settings.prefix == '$' ? 'text-foreground' : ''}`}
|
|
165
106
|
>
|
|
166
107
|
{settings.prefix || settings.suffix}
|
|
167
108
|
</span>
|
|
@@ -169,6 +110,71 @@ export function FieldCurrency({ currency='nzd', currencies, format, onChange, va
|
|
|
169
110
|
)
|
|
170
111
|
}
|
|
171
112
|
|
|
113
|
+
function getCurrencySettings(currency: string, currencies?: Currencies, format?: string, name?: string) {
|
|
114
|
+
// parse CLDR currency string format, e.g. '¤#,##0.00'
|
|
115
|
+
const output: {
|
|
116
|
+
key?: string
|
|
117
|
+
currency: string, // e.g. 'nzd'
|
|
118
|
+
decimalSeparator?: string, // e.g. '.'
|
|
119
|
+
thousandSeparator?: string, // e.g. ','
|
|
120
|
+
minDecimals?: number, // e.g. 2
|
|
121
|
+
maxDecimals?: number, // e.g. 2
|
|
122
|
+
prefix?: string, // e.g. '$'
|
|
123
|
+
suffix?: string // e.g. ''
|
|
124
|
+
} = { currency }
|
|
125
|
+
|
|
126
|
+
let _format = format || defaultFormat
|
|
127
|
+
const _currencies = currencies ?? defaultCurrencies
|
|
128
|
+
const currencyObject = _currencies[currency as keyof typeof _currencies]
|
|
129
|
+
if (!currencyObject && currencies) {
|
|
130
|
+
console.error(
|
|
131
|
+
`The currency field "${name||''}" is using the currency "${currency}" which is not found in your currencies object`
|
|
132
|
+
)
|
|
133
|
+
} else if (!currencyObject && !currencies) {
|
|
134
|
+
console.error(
|
|
135
|
+
`The currency field "${name||''}" is using the currency "${currency}" which is not found in the
|
|
136
|
+
default currencies, please provide a currencies object.`
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
const symbol = currencyObject ? currencyObject.symbol : ''
|
|
140
|
+
const digits = currencyObject ? currencyObject.digits : 2
|
|
141
|
+
|
|
142
|
+
// Check for currency symbol (¤) and determine its position
|
|
143
|
+
if (_format.indexOf('¤') !== -1) {
|
|
144
|
+
const position = _format.indexOf('¤') === 0 ? 'prefix' : 'suffix'
|
|
145
|
+
output[position] = symbol
|
|
146
|
+
_format = _format.replace('¤', '')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Find and set the thousands separator
|
|
150
|
+
const thousandMatch = _format.match(/[^0-9#]/)
|
|
151
|
+
if (thousandMatch) output.thousandSeparator = thousandMatch[0]
|
|
152
|
+
|
|
153
|
+
// Find and set the decimal separator and fraction digits
|
|
154
|
+
const decimalMatch = _format.match(/0[^0-9]/)
|
|
155
|
+
if (decimalMatch) {
|
|
156
|
+
output.decimalSeparator = decimalMatch[0].slice(1)
|
|
157
|
+
if (typeof digits !== 'undefined') {
|
|
158
|
+
output.minDecimals = digits
|
|
159
|
+
output.maxDecimals = digits
|
|
160
|
+
} else {
|
|
161
|
+
const fractionDigits = _format.split(output.decimalSeparator)[1]
|
|
162
|
+
if (fractionDigits) {
|
|
163
|
+
output.minDecimals = fractionDigits.length
|
|
164
|
+
output.maxDecimals = fractionDigits.length
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// create stable key from final output
|
|
170
|
+
output.key = Object.keys(output)
|
|
171
|
+
.filter(k => k !== 'key')
|
|
172
|
+
.sort()
|
|
173
|
+
.map(k => `${k}:${output[k as keyof typeof output] ?? ''}`)
|
|
174
|
+
.join('|')
|
|
175
|
+
return output
|
|
176
|
+
}
|
|
177
|
+
|
|
172
178
|
export const defaultCurrencies: { [key: string]: { name: string, symbol: string, digits: number } } = {
|
|
173
179
|
nzd: { name: 'New Zealand Dollar', symbol: '$', digits: 2 },
|
|
174
180
|
aud: { name: 'Australian Dollar', symbol: '$', digits: 2 },
|
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { format, isValid, parse } from 'date-fns'
|
|
3
|
+
import { tz as _tz, TZDate } from '@date-fns/tz'
|
|
3
4
|
import { getPrefixWidth } from 'nitro-web/util'
|
|
4
|
-
import { Calendar, Dropdown, DropdownProps } from 'nitro-web'
|
|
5
|
+
import { Button, Calendar, Dropdown, DropdownProps, TimePicker } from 'nitro-web'
|
|
5
6
|
import { DayPickerProps } from '../element/calendar'
|
|
6
|
-
import { TimePicker } from './field-time'
|
|
7
7
|
|
|
8
|
-
type
|
|
8
|
+
type Timestamp = null | number
|
|
9
|
+
type TimestampArray = null | Timestamp[]
|
|
9
10
|
type DropdownRef = {
|
|
10
11
|
setIsActive: (value: boolean) => void
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
type PreFieldDateProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> & {
|
|
14
|
+
type PreFieldDateProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value' | 'defaultValue'> & {
|
|
14
15
|
/** field name or path on state (used to match errors), e.g. 'date', 'company.email' **/
|
|
15
16
|
name: string
|
|
16
|
-
/** mode of the date picker */
|
|
17
|
-
mode: Mode
|
|
18
17
|
/** name is used as the id if not provided */
|
|
19
18
|
id?: string
|
|
20
|
-
/**
|
|
19
|
+
/** mode of the date picker */
|
|
20
|
+
mode: 'single' | 'multiple' | 'range' | 'time'
|
|
21
|
+
/** show the time picker for single mode*/
|
|
21
22
|
showTime?: boolean
|
|
22
23
|
/** prefix to add to the input */
|
|
23
24
|
prefix?: string
|
|
@@ -31,116 +32,150 @@ type PreFieldDateProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onCh
|
|
|
31
32
|
DayPickerProps?: DayPickerProps
|
|
32
33
|
/** Dropdown props */
|
|
33
34
|
DropdownProps?: DropdownProps
|
|
35
|
+
/** timezone to use for the date picker */
|
|
36
|
+
tz?: string
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
// Discriminated union types based on mode
|
|
40
|
+
type FieldDatePropsSingle = PreFieldDateProps & {
|
|
41
|
+
mode: 'single' | 'time'
|
|
42
|
+
defaultValue?: Timestamp
|
|
43
|
+
onChange?: (e: { target: { name: string, value: Timestamp } }) => void
|
|
44
|
+
value?: Timestamp // gracefully handles falsey values
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type FieldDatePropsMultiple = PreFieldDateProps & {
|
|
48
|
+
mode: 'multiple' | 'range'
|
|
49
|
+
defaultValue?: TimestampArray
|
|
50
|
+
onChange?: (e: { target: { name: string, value: TimestampArray } }) => void
|
|
51
|
+
value?: TimestampArray // gracefully handles falsey values
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type FieldDateProps = FieldDatePropsSingle | FieldDatePropsMultiple
|
|
55
|
+
const errors: string[] = []
|
|
47
56
|
|
|
48
57
|
export function FieldDate({
|
|
49
58
|
dir = 'bottom-left',
|
|
50
59
|
Icon,
|
|
51
|
-
mode,
|
|
52
60
|
numberOfMonths,
|
|
53
61
|
onChange: onChangeProp,
|
|
54
62
|
prefix = '',
|
|
55
63
|
showTime,
|
|
56
|
-
value: valueProp,
|
|
57
64
|
DayPickerProps,
|
|
58
65
|
DropdownProps,
|
|
66
|
+
tz,
|
|
59
67
|
...props
|
|
60
68
|
}: FieldDateProps) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const localePattern = `d MMM yyyy${showTime && mode == 'single' ? ' hh:mmaa' : ''}`
|
|
69
|
+
const [month, setMonth] = useState<number|undefined>()
|
|
70
|
+
const [preventInputValueUpdates, setPreventInputValueUpdates] = useState(false)
|
|
64
71
|
const [prefixWidth, setPrefixWidth] = useState(0)
|
|
65
72
|
const dropdownRef = useRef<DropdownRef>(null)
|
|
66
|
-
const
|
|
67
|
-
const [lastUpdated, setLastUpdated] = useState(0)
|
|
73
|
+
const pattern = props.mode == 'time' ? 'hh:mmaa' : `d MMM yyyy${showTime && props.mode == 'single' ? ' hh:mmaa' : ''}`
|
|
68
74
|
const id = props.id || props.name
|
|
69
75
|
|
|
70
|
-
// Since value and onChange are optional, we need to
|
|
71
|
-
const [internalValue, setInternalValue] = useState<
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// Update the date's inputValue (text) when the value changes outside of the component
|
|
76
|
+
// Since value and onChange are optional, we need need to create an internal value state
|
|
77
|
+
const [internalValue, setInternalValue] = useState<Timestamp[]>(() => preInternalValue(props))
|
|
78
|
+
const inputValue = useMemo(() => getInputValue(internalValue), [internalValue])
|
|
79
|
+
const [inputValueSticky, setInputValueSticky] = useState(() => inputValue)
|
|
80
|
+
|
|
81
|
+
// Update the internal value when the value changes outside of the component
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
const newValue = preInternalValue(props)
|
|
84
|
+
for (let i=0; i<Math.max(internalValue.length, newValue.length); i++) {
|
|
85
|
+
if (internalValue[i] !== newValue[i]) {
|
|
86
|
+
setInternalValue(newValue)
|
|
87
|
+
break
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}, [props.value])
|
|
91
|
+
|
|
92
|
+
// Only update the sticky input when the input is blurred
|
|
89
93
|
useEffect(() => {
|
|
90
|
-
if (
|
|
91
|
-
}, [
|
|
94
|
+
if (!preventInputValueUpdates) setInputValueSticky(inputValue)
|
|
95
|
+
}, [inputValue, preventInputValueUpdates])
|
|
92
96
|
|
|
93
97
|
// Get the prefix content width
|
|
94
98
|
useEffect(() => {
|
|
95
99
|
setPrefixWidth(getPrefixWidth(prefix, 4))
|
|
96
100
|
}, [prefix])
|
|
97
101
|
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
function preInternalValue(props: FieldDateProps) {
|
|
103
|
+
// Even though we are using types, the value may be different coming from the state, so lets sanitise/parse it.
|
|
104
|
+
// We need to use props.* to get type narrowing.
|
|
105
|
+
switch (props.mode) {
|
|
106
|
+
case 'single':
|
|
107
|
+
case 'time': {
|
|
108
|
+
const value = props?.value ?? props?.defaultValue
|
|
109
|
+
return [value && isValid(value) ? new Date(value).getTime() : null]
|
|
110
|
+
}
|
|
111
|
+
case 'multiple':
|
|
112
|
+
case 'range': {
|
|
113
|
+
const value = props.value ?? props?.defaultValue
|
|
114
|
+
if (!value || !Array.isArray(value)) {
|
|
115
|
+
const error = `FieldDate: ${props.name} value needs to be an array for mode 'multiple' or 'range', received`
|
|
116
|
+
if (value && !errors.includes(error)) { errors.push(error); console.error(error, value) }
|
|
117
|
+
return []
|
|
118
|
+
} else {
|
|
119
|
+
return value.map((timestamp) => {
|
|
120
|
+
return timestamp && isValid(timestamp) ? new Date(timestamp).getTime() : null
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getInputValue(value: Timestamp[]) {
|
|
128
|
+
return value.map(o => date(o, pattern, tz)).join(props.mode == 'range' ? ' - ' : ', ')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function onChange<T>(value: T) {
|
|
132
|
+
if (props.mode == 'single' && !showTime) dropdownRef.current?.setIsActive(false) // Close the dropdown
|
|
133
|
+
if (onChangeProp) onChangeProp({ target: { name: props.name, value: value as any } }) // type enforced in the parameter
|
|
134
|
+
else setInternalValue(preInternalValue({ ...props, value: value } as FieldDateProps))
|
|
104
135
|
}
|
|
105
136
|
|
|
106
137
|
function onInputChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
107
138
|
// Calls onChange (should update state, thus updating the value) with "raw" values
|
|
108
|
-
|
|
139
|
+
setInputValueSticky(e.target.value) // keep the sticky input value in sync
|
|
140
|
+
setPreventInputValueUpdates(true)
|
|
109
141
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return
|
|
142
|
+
// Parse the datestring into timestamps
|
|
143
|
+
let timestamps = e.target.value.split(/-|,/).map(o => {
|
|
144
|
+
return parseDateString(o.trim(), pattern, tz)
|
|
113
145
|
})
|
|
114
|
-
|
|
115
|
-
// For single/range we need limit the array
|
|
116
|
-
if (mode == 'range' && split.length > 1) split.length = 2
|
|
117
|
-
else if (mode == 'multiple') split = split.filter(o => o) // remove invalid dates
|
|
118
146
|
|
|
119
|
-
//
|
|
120
|
-
if (mode == 'range' &&
|
|
147
|
+
// For range mode we need limit the array to 2
|
|
148
|
+
if (props.mode == 'range' && timestamps.length > 1) timestamps.length = 2
|
|
149
|
+
|
|
150
|
+
// Swap range dates if needed
|
|
151
|
+
if (props.mode == 'range' && (timestamps[0] || 0) > (timestamps[1] || 0)) timestamps = [timestamps[0], timestamps[0]]
|
|
121
152
|
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
153
|
+
// Remove/nullify invalid dates
|
|
154
|
+
if (props.mode == 'range') timestamps = timestamps.map(o => o ?? null)
|
|
155
|
+
else if (props.mode == 'multiple') timestamps = timestamps.filter(o => o)
|
|
156
|
+
|
|
157
|
+
// Set month for date mode
|
|
158
|
+
if (props.mode != 'time') {
|
|
159
|
+
for (let i=timestamps.length; i--;) {
|
|
160
|
+
if (timestamps[i]) setMonth(timestamps[i] as number)
|
|
161
|
+
break
|
|
162
|
+
}
|
|
126
163
|
}
|
|
127
164
|
|
|
128
165
|
// Update the value
|
|
129
|
-
const value = mode == 'single' ?
|
|
130
|
-
onChange(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const _dates = Array.isArray(value) ? value : [value]
|
|
136
|
-
return _dates.map(o => o ? format(o, localePattern) : '').join(mode == 'range' ? ' - ' : ', ')
|
|
166
|
+
const value = props.mode == 'single' || props.mode == 'time' ? timestamps[0] ?? null : timestamps
|
|
167
|
+
onChange(value)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function onNowClick() {
|
|
171
|
+
onChange(new Date().getTime())
|
|
137
172
|
}
|
|
138
173
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
174
|
+
// Common props for the Calendar component
|
|
175
|
+
const commonCalendarProps = {
|
|
176
|
+
className: 'pt-1 pb-2 px-3', month: month, numberOfMonths: numberOfMonths, preserveTime: !!showTime, tz: tz, ...DayPickerProps,
|
|
142
177
|
}
|
|
143
|
-
|
|
178
|
+
|
|
144
179
|
return (
|
|
145
180
|
<Dropdown
|
|
146
181
|
ref={dropdownRef}
|
|
@@ -148,20 +183,34 @@ export function FieldDate({
|
|
|
148
183
|
// animate={false}
|
|
149
184
|
// menuIsOpen={true}
|
|
150
185
|
minWidth={0}
|
|
186
|
+
dir={dir}
|
|
151
187
|
menuContent={
|
|
152
|
-
<div
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
188
|
+
<div>
|
|
189
|
+
<div className="flex">
|
|
190
|
+
{
|
|
191
|
+
props.mode == 'single' &&
|
|
192
|
+
<Calendar {...commonCalendarProps} mode="single" value={internalValue[0]} onChange={onChange<Timestamp>} />
|
|
193
|
+
}
|
|
194
|
+
{
|
|
195
|
+
(props.mode == 'range' || props.mode == 'multiple') &&
|
|
196
|
+
<Calendar {...commonCalendarProps} mode={props.mode} value={internalValue} onChange={onChange<TimestampArray>} />
|
|
197
|
+
}
|
|
198
|
+
{
|
|
199
|
+
(props.mode == 'time' || (!!showTime && props.mode == 'single')) &&
|
|
200
|
+
<TimePicker value={internalValue?.[0]} onChange={onChange<Timestamp>}
|
|
201
|
+
className={`border-l border-gray-100 ${props.mode == 'single' ? 'min-h-[0]' : ''}`}
|
|
202
|
+
/>
|
|
203
|
+
}
|
|
204
|
+
</div>
|
|
205
|
+
{
|
|
206
|
+
props.mode == 'time' &&
|
|
207
|
+
<div className="flex justify-between p-2 border-t border-gray-100">
|
|
208
|
+
<Button color="secondary" size="xs" onClick={() => onNowClick()}>Now</Button>
|
|
209
|
+
<Button color="primary" size="xs" onClick={() => dropdownRef.current?.setIsActive(false)}>Done</Button>
|
|
210
|
+
</div>
|
|
211
|
+
}
|
|
162
212
|
</div>
|
|
163
213
|
}
|
|
164
|
-
dir={dir}
|
|
165
214
|
{...DropdownProps}
|
|
166
215
|
>
|
|
167
216
|
<div className="grid grid-cols-1">
|
|
@@ -173,19 +222,44 @@ export function FieldDate({
|
|
|
173
222
|
{prefix}
|
|
174
223
|
</span>
|
|
175
224
|
}
|
|
176
|
-
<input
|
|
225
|
+
<input
|
|
177
226
|
{...props}
|
|
178
227
|
key={'k' + prefixWidth}
|
|
179
228
|
id={id}
|
|
180
229
|
autoComplete="off"
|
|
181
|
-
className={(props.className||'')}// + props.className?.includes('is-invalid') ? ' is-invalid' : ''}
|
|
182
|
-
onBlur={() => setInputValue(getInputValue(validDates))} // onChange should of updated the value -> validValue by this point
|
|
230
|
+
className={(props.className || '')}// + props.className?.includes('is-invalid') ? ' is-invalid' : ''}
|
|
183
231
|
onChange={onInputChange}
|
|
232
|
+
onBlur={() => setPreventInputValueUpdates(false)}
|
|
184
233
|
style={{ textIndent: prefixWidth + 'px' }}
|
|
185
234
|
type="text"
|
|
186
|
-
value={
|
|
235
|
+
value={inputValueSticky} // allways controlled
|
|
236
|
+
defaultValue={undefined}
|
|
187
237
|
/>
|
|
188
238
|
</div>
|
|
189
239
|
</Dropdown>
|
|
190
240
|
)
|
|
191
241
|
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Parse a date string into a timestamp, optionally, from another timezone
|
|
245
|
+
* @param value - date string to parse
|
|
246
|
+
* @param pattern - date format pattern
|
|
247
|
+
* @param [referenceDate] - required if value doesn't contain a date, e.g. for time only
|
|
248
|
+
* @param [tz] - timezone
|
|
249
|
+
*/
|
|
250
|
+
function parseDateString(value: string, pattern: string, tz?: string, referenceDate?: Date) {
|
|
251
|
+
const parsedDate = parse(value.trim(), pattern, referenceDate ?? new Date(), tz ? { in: _tz(tz) } : undefined)
|
|
252
|
+
if (!isValid(parsedDate)) return null
|
|
253
|
+
else return parsedDate.getTime()
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Returns a formatted date string
|
|
258
|
+
* @param [value] - timestamp or date
|
|
259
|
+
* @param [format] - e.g. "dd mmmm yy" (https://date-fns.org/v4.1.0/docs/format#)
|
|
260
|
+
* @param [tz] - display in this timezone
|
|
261
|
+
*/
|
|
262
|
+
export function date(value?: null|number|Date, pattern?: string, tz?: string) {
|
|
263
|
+
if (!value || !isValid(value)) return ''
|
|
264
|
+
return format((tz ? new TZDate(value as number, tz) : value), pattern ?? 'do MMMM')
|
|
265
|
+
}
|