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.
@@ -1,214 +0,0 @@
1
- import { format, parse } from 'date-fns'
2
- import { Button, Dropdown } from 'nitro-web'
3
- import { dayButtonClassName } from '../element/calendar'
4
-
5
- type Timestamp = number // timestamp on epoch day
6
- type DropdownRef = {
7
- setIsActive: (value: boolean) => void
8
- }
9
- export type FieldTimeProps = React.InputHTMLAttributes<HTMLInputElement> & {
10
- name: string
11
- id?: string
12
- onChange?: (e: { target: { name: string, value: null|number } }) => void
13
- value?: string | Timestamp;
14
- Icon?: React.ReactNode
15
- dir?: 'bottom-left'|'bottom-right'|'top-left'|'top-right'
16
- // tz?: string
17
- }
18
- type TimePickerProps = {
19
- date?: Date
20
- onChange: (value: Timestamp) => void
21
- // tz?: string
22
- }
23
-
24
- export function FieldTime({ onChange, value, Icon, dir = 'bottom-left', ...props }: FieldTimeProps) {
25
- // time is viewed and set in local timezone, and saved as timestamp on epoch day.
26
- // Note: timestamp is better than saving seconds so we can easily view this in a particular timezone
27
- const localePattern = 'hh:mmaa'
28
- const dropdownRef = useRef<DropdownRef>(null)
29
- const id = props.id || props.name
30
-
31
- // Convert the value to a valid time value
32
- const validValue = useMemo(() => {
33
- const num = typeof value === 'string' ? parseInt(value) : value
34
- return typeof num === 'number' && !isNaN(num) ? num : new Date(0).getTime()
35
- }, [value])
36
-
37
- // Hold the input value in state
38
- const [inputValue, setInputValue] = useState(() => getInputValue(validValue))
39
-
40
- function onTimePickerChange(value: Timestamp) {
41
- setInputValue(getInputValue(value))
42
- if (onChange) onChange({ target: { name: props.name, value: value }})
43
- }
44
-
45
- function getInputValue(timestamp: Timestamp) {
46
- // Get the input-value in local timezone
47
- return typeof timestamp === 'number' ? format(new Date(timestamp), localePattern) : ''
48
- }
49
-
50
- function onInputChange(e: React.ChangeEvent<HTMLInputElement>) {
51
- // Assume the string is in local timezone, and calls onChange with "raw" values (should update state, thus updating the value).
52
- setInputValue(e.target.value) // keep the input value in sync
53
- const [, _hour, _minute, _second, _period] = e.target.value.match(/(\d{1,2}):(\d{2})(:\d{2})?\s*(am|pm)/i) || []
54
- if (!_hour || !_minute) return
55
- const hour24 = parseInt(_hour) < 12 && _period.match(/pm/i) ? parseInt(_hour) + 12 : parseInt(_hour)
56
- const minute = parseInt(_minute)
57
-
58
- // Assume the time string is in the local timezone, and convert to UTC date from epoch
59
- const localDate = new Date(0)
60
- localDate.setHours(hour24, minute, _second ? parseInt(_second) : 0, 0)
61
- const value = localDate.getTime()
62
- if (onChange) onChange({ target: { name: props.name, value: value }})
63
- }
64
-
65
- function onNowClick() {
66
- const epochDay = new Date(0)
67
- const now = new Date()
68
- // now set hours, minutes, seconds to now but on epoch day
69
- epochDay.setHours(now.getHours(), now.getMinutes(), now.getSeconds(), 0)
70
- onTimePickerChange(epochDay.getTime())
71
- }
72
-
73
- return (
74
- <Dropdown
75
- ref={dropdownRef}
76
- menuToggles={false}
77
- // animate={false}
78
- // menuIsOpen={true}
79
- minWidth={0}
80
- dir={dir}
81
- menuContent={
82
- <div>
83
- <div className="flex justify-center h-[250px]">
84
- <TimePicker date={new Date(validValue)} onChange={onTimePickerChange} />
85
- </div>
86
- <div className="flex justify-between p-2 border-t border-gray-100">
87
- <Button color="secondary" size="xs" onClick={() => onNowClick()}>Now</Button>
88
- <Button color="primary" size="xs" onClick={() => dropdownRef.current?.setIsActive(false)}>Done</Button>
89
- </div>
90
- </div>
91
- }
92
- >
93
- <div className="grid grid-cols-1">
94
- {Icon}
95
- <input
96
- {...props}
97
- id={id}
98
- autoComplete="off"
99
- className={(props.className||'')}// + props.className?.includes('is-invalid') ? ' is-invalid' : ''}
100
- value={inputValue}
101
- onChange={onInputChange}
102
- onBlur={() => setInputValue(getInputValue(validValue))} // onChange should of updated the value -> validValue by this point
103
- type="text"
104
- />
105
- </div>
106
- </Dropdown>
107
- )
108
- }
109
-
110
- export function TimePicker({ date, onChange }: TimePickerProps) {
111
- const refs = {
112
- hour: useRef<HTMLDivElement>(null),
113
- minute: useRef<HTMLDivElement>(null),
114
- period: useRef<HTMLDivElement>(null),
115
- }
116
- const lists = [
117
- [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], // hours
118
- [
119
- 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
120
- 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
121
- 51, 52, 53, 54, 55, 56, 57, 58, 59,
122
- ], // minutes
123
- ['AM', 'PM'], // AM/PM
124
- ]
125
-
126
- // Get current values from date or use defaults
127
- const hour = date ? parseInt(format(date, 'h')) : undefined
128
- const minute = date ? parseInt(format(date, 'm')) : undefined
129
- const period = date ? format(date, 'a') : undefined
130
-
131
- // Scroll into view when the date changes
132
- useEffect(() => {
133
- if (hour !== undefined) scrollIntoView('hour', hour)
134
- if (minute !== undefined) scrollIntoView('minute', minute)
135
- if (period) scrollIntoView('period', period)
136
- }, [date])
137
-
138
- const handleTimeChange = (type: 'hour' | 'minute' | 'period', value: string | number) => {
139
- // Creates a new date object in the local timezone, and calls onChange with the timestamp
140
- const newDate = new Date(date || new Date())
141
-
142
- if (type === 'hour') {
143
- // Parse the time with the new hour value
144
- const timeString = `${value}:${format(newDate, 'mm')} ${format(newDate, 'a')}`
145
- const updatedDate = parse(timeString, 'h:mm a', newDate)
146
- newDate.setHours(updatedDate.getHours(), updatedDate.getMinutes())
147
- } else if (type === 'minute') {
148
- // Parse the time with the new minute value
149
- const timeString = `${format(newDate, 'h')}:${value} ${format(newDate, 'a')}`
150
- const updatedDate = parse(timeString, 'h:mm a', newDate)
151
- newDate.setMinutes(updatedDate.getMinutes())
152
- } else if (type === 'period') {
153
- // Parse the time with the new period value
154
- const timeString = `${format(newDate, 'h')}:${format(newDate, 'mm')} ${value}`
155
- const updatedDate = parse(timeString, 'h:mm a', newDate)
156
- newDate.setHours(updatedDate.getHours())
157
- }
158
-
159
- onChange(newDate.getTime())
160
- }
161
-
162
- function scrollIntoView (type: 'hour' | 'minute' | 'period', value: string | number) {
163
- const container = refs[type].current
164
- if (!container) return
165
- const element = container.querySelector(`[data-val="${value}"]`) as HTMLElement
166
- if (!element) return
167
-
168
- const target =
169
- element.offsetTop
170
- - (container.clientHeight / 2)
171
- + (element.clientHeight / 2)
172
-
173
- container.scrollTo({ top: target, behavior: 'smooth' })
174
- }
175
-
176
- return (
177
- lists.map((list, i) => {
178
- const type = i === 0 ? 'hour' : i === 1 ? 'minute' : 'period'
179
- const currentValue = i === 0 ? hour : i === 1 ? minute : period
180
-
181
- return (
182
- <div
183
- key={i}
184
- ref={refs[type]}
185
- className="w-[60px] relative overflow-hidden hover:overflow-y-auto border-l border-gray-100 sm-scrollbar first:border-l-0"
186
- >
187
- <div className="w-[60px] absolute flex flex-col items-center py-2">
188
- {/* using absolute since the scrollbar takes up space */}
189
- {list.map(item => (
190
- <div
191
- className="py-[1px] flex group cursor-pointer"
192
- data-val={item}
193
- key={item}
194
- onClick={(_e) => {
195
- handleTimeChange(type, item)
196
- }}
197
- >
198
- <button
199
- key={item}
200
- className={
201
- `${dayButtonClassName} rounded-full flex justify-center items-center group-hover:bg-gray-100 `
202
- + (item === currentValue ? '!bg-input-border-focus text-white' : '')
203
- }
204
- >
205
- {item.toString().padStart(2, '0').toLowerCase()}
206
- </button>
207
- </div>
208
- ))}
209
- </div>
210
- </div>
211
- )
212
- })
213
- )
214
- }