@startupjs-ui/date-time-picker 0.1.3
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/CHANGELOG.md +20 -0
- package/README.mdx +86 -0
- package/components/Calendar/Days/index.cssx.styl +47 -0
- package/components/Calendar/Days/index.tsx +133 -0
- package/components/Calendar/Header/index.cssx.styl +40 -0
- package/components/Calendar/Header/index.tsx +159 -0
- package/components/Calendar/index.tsx +57 -0
- package/components/TimeSelect/index.cssx.styl +23 -0
- package/components/TimeSelect/index.tsx +122 -0
- package/components/index.ts +2 -0
- package/helpers/getLocale.ts +15 -0
- package/helpers/index.ts +1 -0
- package/index.cssx.styl +53 -0
- package/index.d.ts +72 -0
- package/index.tsx +485 -0
- package/package.json +33 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
+
|
|
6
|
+
## [0.1.3](https://github.com/startupjs/startupjs-ui/compare/v0.1.2...v0.1.3) (2025-12-29)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @startupjs-ui/date-time-picker
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [0.1.2](https://github.com/startupjs/startupjs-ui/compare/v0.1.1...v0.1.2) (2025-12-29)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* add mdx and docs packages. Refactor docs to get rid of any @startupjs/ui usage and use startupjs-ui instead ([703c926](https://github.com/startupjs/startupjs-ui/commit/703c92636efb0421ffd11783f692fc892b74018f))
|
|
20
|
+
* **date-time-picker:** refactor DateTimePicker component. TODO: fix moment.tz.guess() by loading timezone data correctly. ([ae39c14](https://github.com/startupjs/startupjs-ui/commit/ae39c1450ce6d57e6323c4777318de1e57667041))
|
package/README.mdx
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { $ } from 'startupjs'
|
|
3
|
+
import DateTimePicker, { _PropsJsonSchema as DateTimePickerPropsJsonSchema } from './index'
|
|
4
|
+
import { Sandbox } from '@startupjs-ui/docs'
|
|
5
|
+
|
|
6
|
+
# DateTimePicker
|
|
7
|
+
|
|
8
|
+
DateTimePicker allows to display and change the date and time.
|
|
9
|
+
|
|
10
|
+
```jsx
|
|
11
|
+
import { DateTimePicker } from 'startupjs-ui'
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Simple example
|
|
15
|
+
|
|
16
|
+
```jsx example
|
|
17
|
+
const [date, setDate] = useState(+new Date())
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<DateTimePicker
|
|
21
|
+
date={date}
|
|
22
|
+
onChangeDate={setDate}
|
|
23
|
+
/>
|
|
24
|
+
)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Managing visibility
|
|
28
|
+
|
|
29
|
+
There are three options for managing visiblity of a `DateTimePicker`:
|
|
30
|
+
|
|
31
|
+
1. By passing the scoped model to the `$visible` property from the state of which visibility is controlled.
|
|
32
|
+
|
|
33
|
+
```jsx example
|
|
34
|
+
const $visible = $()
|
|
35
|
+
const [date, setDate] = useState()
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<DateTimePicker
|
|
39
|
+
date={date}
|
|
40
|
+
$visible={$visible}
|
|
41
|
+
onChangeDate={setDate}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
2. By passing the `visible`, `onRequestOpen` and `onRequestClose` properties that determines whether `DateTimePicker` is visible.
|
|
47
|
+
|
|
48
|
+
```jsx example
|
|
49
|
+
const $visible = $()
|
|
50
|
+
const [date, setDate] = useState()
|
|
51
|
+
const visible = $visible.get()
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<DateTimePicker
|
|
55
|
+
date={date}
|
|
56
|
+
visible={visible}
|
|
57
|
+
onChangeDate={setDate}
|
|
58
|
+
onRequestOpen={() => $visible.set(true)}
|
|
59
|
+
onRequestClose={() => $visible.del()}
|
|
60
|
+
/>
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
3. Uncontrolled.
|
|
65
|
+
|
|
66
|
+
```jsx example
|
|
67
|
+
const [date, setDate] = useState()
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<DateTimePicker
|
|
71
|
+
date={date}
|
|
72
|
+
onChangeDate={setDate}
|
|
73
|
+
/>
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Sandbox
|
|
78
|
+
|
|
79
|
+
<Sandbox
|
|
80
|
+
Component={DateTimePicker}
|
|
81
|
+
propsJsonSchema={DateTimePickerPropsJsonSchema}
|
|
82
|
+
props={{
|
|
83
|
+
date: +new Date()
|
|
84
|
+
}}
|
|
85
|
+
block
|
|
86
|
+
/>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
.shortName
|
|
2
|
+
color var(--color-text-description)
|
|
3
|
+
font(caption)
|
|
4
|
+
|
|
5
|
+
.label
|
|
6
|
+
font(caption)
|
|
7
|
+
|
|
8
|
+
&.isMute
|
|
9
|
+
color var(--color-text-description)
|
|
10
|
+
|
|
11
|
+
&.isActive
|
|
12
|
+
color var(--color-text-on-primary)
|
|
13
|
+
|
|
14
|
+
.cell
|
|
15
|
+
justify-content center
|
|
16
|
+
align-items center
|
|
17
|
+
width 4u
|
|
18
|
+
height 4u
|
|
19
|
+
margin .5u
|
|
20
|
+
radius(circle)
|
|
21
|
+
&:part(hover)
|
|
22
|
+
background-color var(--color-bg-primary-transparent)
|
|
23
|
+
|
|
24
|
+
&.isActive
|
|
25
|
+
background-color var(--color-bg-primary)
|
|
26
|
+
&:part(hover)
|
|
27
|
+
background-color var(--color-bg-primary-subtle)
|
|
28
|
+
|
|
29
|
+
&.isActiveRangeStart
|
|
30
|
+
border-top-right-radius 0
|
|
31
|
+
border-bottom-right-radius 0
|
|
32
|
+
background-color var(--color-bg-primary)
|
|
33
|
+
|
|
34
|
+
&.isActiveRangeEnd
|
|
35
|
+
border-top-left-radius 0
|
|
36
|
+
border-bottom-left-radius 0
|
|
37
|
+
background-color var(--color-bg-primary)
|
|
38
|
+
|
|
39
|
+
&.isActiveRange
|
|
40
|
+
border-radius 0
|
|
41
|
+
background-color var(--color-bg-primary-subtle)
|
|
42
|
+
|
|
43
|
+
.row
|
|
44
|
+
justify-content center
|
|
45
|
+
|
|
46
|
+
+from($UI.media.tablet)
|
|
47
|
+
justify-content flex-start
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { useMemo, useCallback, type ReactNode } from 'react'
|
|
2
|
+
import { pug, observer } from 'startupjs'
|
|
3
|
+
import Span from '@startupjs-ui/span'
|
|
4
|
+
import Div from '@startupjs-ui/div'
|
|
5
|
+
import moment from 'moment-timezone'
|
|
6
|
+
import './index.cssx.styl'
|
|
7
|
+
|
|
8
|
+
interface DaysProps {
|
|
9
|
+
date: Date
|
|
10
|
+
uiDate: number
|
|
11
|
+
exactLocale: string
|
|
12
|
+
timezone: string
|
|
13
|
+
disabledDays: number[]
|
|
14
|
+
maxDate?: number
|
|
15
|
+
minDate?: number
|
|
16
|
+
range?: [number, number]
|
|
17
|
+
onChangeDate?: (value: number) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function Days ({
|
|
21
|
+
date,
|
|
22
|
+
uiDate,
|
|
23
|
+
exactLocale,
|
|
24
|
+
timezone,
|
|
25
|
+
disabledDays,
|
|
26
|
+
maxDate,
|
|
27
|
+
minDate,
|
|
28
|
+
range,
|
|
29
|
+
onChangeDate
|
|
30
|
+
}: DaysProps): ReactNode {
|
|
31
|
+
const weekdaysShort = useMemo(() => {
|
|
32
|
+
const data = (moment
|
|
33
|
+
.tz(uiDate, timezone)
|
|
34
|
+
.locale(exactLocale) as any)
|
|
35
|
+
._locale
|
|
36
|
+
._weekdaysShort
|
|
37
|
+
|
|
38
|
+
return data.map((day: string) => day.toUpperCase())
|
|
39
|
+
}, [uiDate, timezone, exactLocale])
|
|
40
|
+
|
|
41
|
+
const matrixMonthDays = useMemo(() => {
|
|
42
|
+
const data = []
|
|
43
|
+
|
|
44
|
+
const nowDate = moment.tz(timezone)
|
|
45
|
+
|
|
46
|
+
const currentDay = moment
|
|
47
|
+
.tz(uiDate, timezone)
|
|
48
|
+
.startOf('M')
|
|
49
|
+
.startOf('w')
|
|
50
|
+
.hours(nowDate.hours())
|
|
51
|
+
.minutes(nowDate.minutes())
|
|
52
|
+
.seconds(nowDate.seconds())
|
|
53
|
+
.milliseconds(nowDate.milliseconds())
|
|
54
|
+
|
|
55
|
+
for (let weekIndex = 0; weekIndex < 6; weekIndex++) {
|
|
56
|
+
const weekLine = []
|
|
57
|
+
for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
|
|
58
|
+
weekLine.push({
|
|
59
|
+
label: currentDay.format('DD'),
|
|
60
|
+
month: currentDay.month(),
|
|
61
|
+
day: currentDay.date(),
|
|
62
|
+
value: +currentDay,
|
|
63
|
+
testID: `${currentDay.format('MM')}-` +
|
|
64
|
+
`${currentDay.format('DD')}-${currentDay.format('YYYY')}`
|
|
65
|
+
})
|
|
66
|
+
currentDay.add(1, 'd')
|
|
67
|
+
}
|
|
68
|
+
data.push(weekLine)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return data
|
|
72
|
+
}, [uiDate, timezone])
|
|
73
|
+
|
|
74
|
+
function _onChangeDay (item: any) {
|
|
75
|
+
const timestamp = +moment
|
|
76
|
+
.tz(uiDate, timezone)
|
|
77
|
+
.date(item.day)
|
|
78
|
+
.month(item.month)
|
|
79
|
+
|
|
80
|
+
onChangeDate && onChangeDate(timestamp)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const isDisableDay = useCallback((value: number) => {
|
|
84
|
+
const isDisabledDay = disabledDays.some(item => moment.tz(item, timezone).isSame(value, 'd'))
|
|
85
|
+
const isBeforeMinDate = minDate != null
|
|
86
|
+
? moment.tz(minDate, timezone).isAfter(value, 'd')
|
|
87
|
+
: false
|
|
88
|
+
const isAfterMaxDate = maxDate != null
|
|
89
|
+
? moment.tz(maxDate, timezone).isBefore(value, 'd')
|
|
90
|
+
: false
|
|
91
|
+
|
|
92
|
+
return isDisabledDay || isBeforeMinDate || isAfterMaxDate
|
|
93
|
+
}, [disabledDays, maxDate, minDate, timezone])
|
|
94
|
+
|
|
95
|
+
function getLabelActive (value: number) {
|
|
96
|
+
return range
|
|
97
|
+
? moment.tz(value, timezone).isSame(range[0], 'd') ||
|
|
98
|
+
moment.tz(value, timezone).isSame(range[1], 'd')
|
|
99
|
+
: moment.tz(value, timezone).isSame(date, 'd')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return pug`
|
|
103
|
+
Div.row(row)
|
|
104
|
+
for shortDayName in weekdaysShort
|
|
105
|
+
Div.cell(key=shortDayName)
|
|
106
|
+
Span.shortName(bold)= shortDayName
|
|
107
|
+
|
|
108
|
+
for week, weekIndex in matrixMonthDays
|
|
109
|
+
Div.row(key='week-' + weekIndex row)
|
|
110
|
+
for day, dayIndex in matrixMonthDays[weekIndex]
|
|
111
|
+
Div.cell(
|
|
112
|
+
key=weekIndex + '-' + dayIndex
|
|
113
|
+
styleName={
|
|
114
|
+
isActive: !range && moment.tz(day.value, timezone).isSame(date, 'd'),
|
|
115
|
+
isActiveRangeStart: range && moment.tz(day.value, timezone).isSame(range[0], 'd'),
|
|
116
|
+
isActiveRange: range && moment.tz(day.value, timezone).isBetween(range[0], range[1], 'd'),
|
|
117
|
+
isActiveRangeEnd: range && moment.tz(day.value, timezone).isSame(range[1], 'd')
|
|
118
|
+
}
|
|
119
|
+
disabled=isDisableDay(day.value)
|
|
120
|
+
testID=day.testID
|
|
121
|
+
onPress=() => _onChangeDay(day)
|
|
122
|
+
)
|
|
123
|
+
Span.label(
|
|
124
|
+
bold=getLabelActive(day.value)
|
|
125
|
+
styleName={
|
|
126
|
+
isMute: !moment.tz(day.value, timezone).isSame(uiDate, 'M'),
|
|
127
|
+
isActive: getLabelActive(day.value)
|
|
128
|
+
}
|
|
129
|
+
)= day.label
|
|
130
|
+
`
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export default observer(Days)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
.header
|
|
2
|
+
align-items center
|
|
3
|
+
justify-content center
|
|
4
|
+
margin 0 0 .5u 1u
|
|
5
|
+
|
|
6
|
+
+from($UI.media.tablet)
|
|
7
|
+
justify-content space-between
|
|
8
|
+
|
|
9
|
+
.month
|
|
10
|
+
font(h5)
|
|
11
|
+
|
|
12
|
+
.yearText
|
|
13
|
+
color var(--color-text-primary)
|
|
14
|
+
font(h5)
|
|
15
|
+
|
|
16
|
+
.actions
|
|
17
|
+
justify-content flex-end
|
|
18
|
+
margin-left 2u
|
|
19
|
+
|
|
20
|
+
+from($UI.media.tablet)
|
|
21
|
+
margin-left 0
|
|
22
|
+
|
|
23
|
+
.button, .years
|
|
24
|
+
margin-left .5u
|
|
25
|
+
|
|
26
|
+
.years
|
|
27
|
+
&-popover
|
|
28
|
+
padding 0
|
|
29
|
+
max-height 27u
|
|
30
|
+
|
|
31
|
+
&-item
|
|
32
|
+
height 4.5u
|
|
33
|
+
align-items center
|
|
34
|
+
justify-content center
|
|
35
|
+
padding-right 2u
|
|
36
|
+
padding-left @padding-right
|
|
37
|
+
|
|
38
|
+
.year
|
|
39
|
+
font(h5)
|
|
40
|
+
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { useCallback, useMemo, type ReactNode } from 'react'
|
|
2
|
+
import { pug, observer, $ } from 'startupjs'
|
|
3
|
+
import Button from '@startupjs-ui/button'
|
|
4
|
+
import Div from '@startupjs-ui/div'
|
|
5
|
+
import FlatList from '@startupjs-ui/flat-list'
|
|
6
|
+
import Icon from '@startupjs-ui/icon'
|
|
7
|
+
import Popover from '@startupjs-ui/popover'
|
|
8
|
+
import Span from '@startupjs-ui/span'
|
|
9
|
+
import { faAngleLeft } from '@fortawesome/free-solid-svg-icons/faAngleLeft'
|
|
10
|
+
import { faAngleRight } from '@fortawesome/free-solid-svg-icons/faAngleRight'
|
|
11
|
+
import { faCaretDown } from '@fortawesome/free-solid-svg-icons/faCaretDown'
|
|
12
|
+
import moment from 'moment-timezone'
|
|
13
|
+
import STYLES from './index.cssx.styl'
|
|
14
|
+
|
|
15
|
+
const yearsItemStyle = STYLES['years-item']
|
|
16
|
+
const YEAR_ITEM_HEIGHT = yearsItemStyle.height
|
|
17
|
+
|
|
18
|
+
interface HeaderProps {
|
|
19
|
+
uiDate: number
|
|
20
|
+
exactLocale: string
|
|
21
|
+
timezone: string
|
|
22
|
+
minDate?: number
|
|
23
|
+
maxDate?: number
|
|
24
|
+
$uiDate: any
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function Header ({
|
|
28
|
+
uiDate,
|
|
29
|
+
exactLocale,
|
|
30
|
+
timezone,
|
|
31
|
+
minDate,
|
|
32
|
+
maxDate,
|
|
33
|
+
$uiDate
|
|
34
|
+
}: HeaderProps): ReactNode {
|
|
35
|
+
const monthName = moment.tz(uiDate, timezone).locale(exactLocale).format('MMM')
|
|
36
|
+
|
|
37
|
+
const onChangeMonth = useCallback((value: number) => {
|
|
38
|
+
const ts = +moment($uiDate.get()).add('month', value)
|
|
39
|
+
$uiDate.set(ts)
|
|
40
|
+
}, [$uiDate])
|
|
41
|
+
|
|
42
|
+
const isPrevDisabled = minDate
|
|
43
|
+
? +moment.tz($uiDate.get(), timezone).endOf('month').add('month', -1) < minDate
|
|
44
|
+
: false
|
|
45
|
+
|
|
46
|
+
const isNextDisabled = maxDate
|
|
47
|
+
? +moment($uiDate.get()).startOf('month').add('month', 1) > maxDate
|
|
48
|
+
: false
|
|
49
|
+
|
|
50
|
+
return pug`
|
|
51
|
+
Div.header(row)
|
|
52
|
+
Div(vAlign='center' row)
|
|
53
|
+
Span.month(bold)= monthName
|
|
54
|
+
Years.years(
|
|
55
|
+
timezone=timezone
|
|
56
|
+
minDate=minDate
|
|
57
|
+
maxDate=maxDate
|
|
58
|
+
$uiDate=$uiDate
|
|
59
|
+
)
|
|
60
|
+
Div.actions(row)
|
|
61
|
+
Button.button(
|
|
62
|
+
color='text-description'
|
|
63
|
+
variant='text'
|
|
64
|
+
disabled=isPrevDisabled
|
|
65
|
+
icon=faAngleLeft
|
|
66
|
+
onPress=()=> onChangeMonth(-1)
|
|
67
|
+
)
|
|
68
|
+
Button.button(
|
|
69
|
+
color='text-description'
|
|
70
|
+
variant='text'
|
|
71
|
+
disabled=isNextDisabled
|
|
72
|
+
icon=faAngleRight
|
|
73
|
+
onPress=()=> onChangeMonth(1)
|
|
74
|
+
)
|
|
75
|
+
`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default observer(Header)
|
|
79
|
+
|
|
80
|
+
interface YearsProps {
|
|
81
|
+
style?: any
|
|
82
|
+
minDate?: number
|
|
83
|
+
maxDate?: number
|
|
84
|
+
timezone: string
|
|
85
|
+
$uiDate: any
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const Years = observer(function YearsComponent ({
|
|
89
|
+
style,
|
|
90
|
+
minDate,
|
|
91
|
+
maxDate,
|
|
92
|
+
timezone,
|
|
93
|
+
$uiDate
|
|
94
|
+
}: YearsProps): ReactNode {
|
|
95
|
+
const $visible = $(false)
|
|
96
|
+
const minYear = minDate ? moment.tz(minDate, timezone).year() : 1950
|
|
97
|
+
const maxYear = maxDate ? moment.tz(maxDate, timezone).year() : 2050
|
|
98
|
+
const yearsDiff = maxYear - minYear
|
|
99
|
+
|
|
100
|
+
const onChangeYear = useCallback((year: number) => {
|
|
101
|
+
const ts = +moment($uiDate.get()).year(year)
|
|
102
|
+
$uiDate.set(ts)
|
|
103
|
+
$visible.set(false)
|
|
104
|
+
}, [$uiDate, $visible])
|
|
105
|
+
|
|
106
|
+
const years = useMemo(() => {
|
|
107
|
+
return new Array(yearsDiff + 1).fill(minYear).map((year, index) => {
|
|
108
|
+
return year + index
|
|
109
|
+
})
|
|
110
|
+
}, [yearsDiff, minYear])
|
|
111
|
+
|
|
112
|
+
const getItemLayout = useCallback((data: any, index: number) => {
|
|
113
|
+
return {
|
|
114
|
+
offset: YEAR_ITEM_HEIGHT * index,
|
|
115
|
+
length: YEAR_ITEM_HEIGHT,
|
|
116
|
+
index
|
|
117
|
+
}
|
|
118
|
+
}, [])
|
|
119
|
+
|
|
120
|
+
if (!yearsDiff) {
|
|
121
|
+
return pug`
|
|
122
|
+
Div(style=style)
|
|
123
|
+
Span.year(bold)= maxYear
|
|
124
|
+
`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function renderYears (): ReactNode {
|
|
128
|
+
return pug`
|
|
129
|
+
FlatList(
|
|
130
|
+
data=years
|
|
131
|
+
renderItem=renderYear
|
|
132
|
+
keyExtractor=item => item
|
|
133
|
+
getItemLayout=getItemLayout
|
|
134
|
+
)
|
|
135
|
+
`
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function renderYear ({ item }: { item: number }): ReactNode {
|
|
139
|
+
return pug`
|
|
140
|
+
Div.years-item(
|
|
141
|
+
variant='higlight'
|
|
142
|
+
onPress=() => onChangeYear(item)
|
|
143
|
+
)
|
|
144
|
+
Span= item
|
|
145
|
+
`
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return pug`
|
|
149
|
+
Div(style=style)
|
|
150
|
+
Popover(
|
|
151
|
+
$visible=$visible
|
|
152
|
+
renderContent=renderYears
|
|
153
|
+
attachmentStyleName='years-popover'
|
|
154
|
+
)
|
|
155
|
+
Div(vAlign='center' row)
|
|
156
|
+
Span.year(bold)= moment.tz($uiDate.get(), timezone).year()
|
|
157
|
+
Icon(icon=faCaretDown)
|
|
158
|
+
`
|
|
159
|
+
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type ReactNode } from 'react'
|
|
2
|
+
import { pug, observer, $ } from 'startupjs'
|
|
3
|
+
import Div from '@startupjs-ui/div'
|
|
4
|
+
import moment from 'moment-timezone'
|
|
5
|
+
import Header from './Header'
|
|
6
|
+
import Days from './Days'
|
|
7
|
+
|
|
8
|
+
interface CalendarProps {
|
|
9
|
+
date: Date
|
|
10
|
+
disabledDays?: number[]
|
|
11
|
+
exactLocale: string
|
|
12
|
+
timezone: string
|
|
13
|
+
maxDate?: number
|
|
14
|
+
minDate?: number
|
|
15
|
+
range?: [number, number]
|
|
16
|
+
testID?: string
|
|
17
|
+
onChangeDate?: (value: number) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function Calendar ({
|
|
21
|
+
date,
|
|
22
|
+
disabledDays = [],
|
|
23
|
+
exactLocale,
|
|
24
|
+
timezone,
|
|
25
|
+
maxDate,
|
|
26
|
+
minDate,
|
|
27
|
+
range,
|
|
28
|
+
testID,
|
|
29
|
+
onChangeDate
|
|
30
|
+
}: CalendarProps): ReactNode {
|
|
31
|
+
const $uiDate = $(+moment(date).seconds(0).milliseconds(0))
|
|
32
|
+
|
|
33
|
+
return pug`
|
|
34
|
+
Div(testID=testID)
|
|
35
|
+
Header(
|
|
36
|
+
uiDate=$uiDate.get()
|
|
37
|
+
exactLocale=exactLocale
|
|
38
|
+
timezone=timezone
|
|
39
|
+
minDate=minDate
|
|
40
|
+
maxDate=maxDate
|
|
41
|
+
$uiDate=$uiDate
|
|
42
|
+
)
|
|
43
|
+
Days(
|
|
44
|
+
date=date
|
|
45
|
+
uiDate=$uiDate.get()
|
|
46
|
+
exactLocale=exactLocale
|
|
47
|
+
timezone=timezone
|
|
48
|
+
range=range
|
|
49
|
+
minDate=minDate
|
|
50
|
+
maxDate=maxDate
|
|
51
|
+
disabledDays=disabledDays
|
|
52
|
+
onChangeDate=onChangeDate
|
|
53
|
+
)
|
|
54
|
+
`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default observer(Calendar)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
.cell
|
|
2
|
+
height 5u
|
|
3
|
+
width 20u
|
|
4
|
+
align-items center
|
|
5
|
+
justify-content center
|
|
6
|
+
align-self center
|
|
7
|
+
&:part(hover)
|
|
8
|
+
background-color var(--color-bg-primary-transparent)
|
|
9
|
+
|
|
10
|
+
+from($UI.media.tablet)
|
|
11
|
+
width 12u
|
|
12
|
+
|
|
13
|
+
&.isActive
|
|
14
|
+
background-color var(--color-bg-primary)
|
|
15
|
+
&:part(hover)
|
|
16
|
+
background-color var(--color-bg-primary-subtle)
|
|
17
|
+
|
|
18
|
+
.label
|
|
19
|
+
&.isActive
|
|
20
|
+
color var(--color-text-on-primary)
|
|
21
|
+
|
|
22
|
+
:export
|
|
23
|
+
media: $UI.media
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useEffect,
|
|
3
|
+
useMemo,
|
|
4
|
+
useRef,
|
|
5
|
+
useCallback,
|
|
6
|
+
useImperativeHandle,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
type RefObject
|
|
9
|
+
} from 'react'
|
|
10
|
+
import { pug, observer } from 'startupjs'
|
|
11
|
+
import Div from '@startupjs-ui/div'
|
|
12
|
+
import FlatList from '@startupjs-ui/flat-list'
|
|
13
|
+
import Span from '@startupjs-ui/span'
|
|
14
|
+
import moment from 'moment-timezone'
|
|
15
|
+
import STYLES from './index.cssx.styl'
|
|
16
|
+
|
|
17
|
+
export interface TimeSelectRef {
|
|
18
|
+
scrollToIndex: (date?: Date) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface TimeSelectProps {
|
|
22
|
+
date: Date
|
|
23
|
+
exactLocale: string
|
|
24
|
+
timezone: string
|
|
25
|
+
is24Hour?: boolean
|
|
26
|
+
minDate?: number
|
|
27
|
+
maxDate?: number
|
|
28
|
+
timeInterval: number
|
|
29
|
+
onChangeDate?: (value: number) => void
|
|
30
|
+
ref?: RefObject<TimeSelectRef>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// TODO: add displayTimeVariant
|
|
34
|
+
function TimeSelect ({
|
|
35
|
+
date,
|
|
36
|
+
exactLocale,
|
|
37
|
+
timezone,
|
|
38
|
+
is24Hour,
|
|
39
|
+
minDate,
|
|
40
|
+
maxDate,
|
|
41
|
+
timeInterval,
|
|
42
|
+
onChangeDate,
|
|
43
|
+
ref
|
|
44
|
+
}: TimeSelectProps): ReactNode {
|
|
45
|
+
const refScroll = useRef<any>(null)
|
|
46
|
+
|
|
47
|
+
// we are looking for 'a' in current locale
|
|
48
|
+
// to figure out whether to apply 12 hour format
|
|
49
|
+
const _is24Hour = useMemo(() => {
|
|
50
|
+
if (is24Hour != null) return is24Hour
|
|
51
|
+
const lt = (moment().locale(exactLocale) as any)._locale._longDateFormat.LT
|
|
52
|
+
return !/a/i.test(lt)
|
|
53
|
+
}, [is24Hour, exactLocale])
|
|
54
|
+
|
|
55
|
+
const preparedData = useMemo(() => {
|
|
56
|
+
const res: Array<{ label: string, value: number, disabled: boolean }> = []
|
|
57
|
+
|
|
58
|
+
let currentTimestamp = +moment.tz(date, timezone).locale(exactLocale).startOf('d')
|
|
59
|
+
const endTimestamp = +moment.tz(date, timezone).locale(exactLocale).endOf('d')
|
|
60
|
+
const intervalTimestamp = timeInterval * 60 * 1000
|
|
61
|
+
|
|
62
|
+
const format = _is24Hour
|
|
63
|
+
? (moment.tz(date, timezone).locale(exactLocale) as any)._locale._longDateFormat.LT
|
|
64
|
+
: 'hh:mm A'
|
|
65
|
+
|
|
66
|
+
while (currentTimestamp < endTimestamp) {
|
|
67
|
+
res.push({
|
|
68
|
+
label: moment.tz(currentTimestamp, timezone).locale(exactLocale).format(format),
|
|
69
|
+
value: currentTimestamp,
|
|
70
|
+
disabled: (maxDate != null && currentTimestamp > maxDate) ||
|
|
71
|
+
(minDate != null && currentTimestamp < minDate)
|
|
72
|
+
})
|
|
73
|
+
currentTimestamp += intervalTimestamp
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return res
|
|
77
|
+
}, [date, exactLocale, timezone, timeInterval, _is24Hour, maxDate, minDate])
|
|
78
|
+
|
|
79
|
+
const scrollToIndex = useCallback((_date: Date = date) => {
|
|
80
|
+
const dateTimestamp = +moment.tz(_date, timezone)
|
|
81
|
+
const index = preparedData.findIndex(item => dateTimestamp === item.value)
|
|
82
|
+
if (index === -1) return
|
|
83
|
+
refScroll.current?.scrollToIndex?.({ index, animated: false })
|
|
84
|
+
}, [date, timezone, preparedData])
|
|
85
|
+
|
|
86
|
+
useImperativeHandle(ref, () => ({ scrollToIndex }), [scrollToIndex])
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
scrollToIndex()
|
|
89
|
+
}, [scrollToIndex])
|
|
90
|
+
|
|
91
|
+
function renderItem ({ item }: { item: any }): ReactNode {
|
|
92
|
+
const isActive = +moment(date) === item.value
|
|
93
|
+
|
|
94
|
+
return pug`
|
|
95
|
+
Div.cell(
|
|
96
|
+
styleName={ isActive }
|
|
97
|
+
disabled=item.disabled
|
|
98
|
+
onPress=()=> onChangeDate && onChangeDate(item.value)
|
|
99
|
+
)
|
|
100
|
+
Span.label(styleName={ isActive })
|
|
101
|
+
= item.label
|
|
102
|
+
`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const length = STYLES.cell.height
|
|
106
|
+
|
|
107
|
+
return pug`
|
|
108
|
+
FlatList(
|
|
109
|
+
ref=refScroll
|
|
110
|
+
data=preparedData
|
|
111
|
+
renderItem=renderItem
|
|
112
|
+
getItemLayout=(data, index) => ({
|
|
113
|
+
offset: length * index,
|
|
114
|
+
length,
|
|
115
|
+
index
|
|
116
|
+
})
|
|
117
|
+
keyExtractor=item=> String(item.value)
|
|
118
|
+
)
|
|
119
|
+
`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export default observer(TimeSelect)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { I18nManager, Platform, Settings } from 'react-native'
|
|
2
|
+
|
|
3
|
+
export default function getLocale (): string {
|
|
4
|
+
if (Platform.OS === 'web') {
|
|
5
|
+
return typeof window !== 'undefined' ? window.navigator.language : 'en'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (Platform.OS === 'ios') {
|
|
9
|
+
const appleLocale = Settings.get('AppleLocale') as string | undefined
|
|
10
|
+
const appleLanguages = Settings.get('AppleLanguages') as string[] | undefined
|
|
11
|
+
return appleLocale ?? appleLanguages?.[0] ?? 'en'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return (I18nManager.getConstants() as any).localeIdentifier
|
|
15
|
+
}
|
package/helpers/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as getLocale } from './getLocale'
|
package/index.cssx.styl
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
.content
|
|
2
|
+
flex-direction column
|
|
3
|
+
padding 1u
|
|
4
|
+
flex-shrink 1
|
|
5
|
+
|
|
6
|
+
+from($UI.media.tablet)
|
|
7
|
+
flex-direction row
|
|
8
|
+
height 42u
|
|
9
|
+
|
|
10
|
+
.divider
|
|
11
|
+
align-self center
|
|
12
|
+
width 60%
|
|
13
|
+
height 1px
|
|
14
|
+
margin 1u 0
|
|
15
|
+
|
|
16
|
+
+from($UI.media.tablet)
|
|
17
|
+
height 95%
|
|
18
|
+
width 1px
|
|
19
|
+
margin 0 2u
|
|
20
|
+
|
|
21
|
+
.drawer
|
|
22
|
+
height 95%
|
|
23
|
+
border-radius 0
|
|
24
|
+
padding-top 3u
|
|
25
|
+
|
|
26
|
+
+native()
|
|
27
|
+
height auto
|
|
28
|
+
|
|
29
|
+
.actions
|
|
30
|
+
justify-content space-between
|
|
31
|
+
|
|
32
|
+
.rnPicker
|
|
33
|
+
margin-top 2u
|
|
34
|
+
|
|
35
|
+
.swipe
|
|
36
|
+
height 3u
|
|
37
|
+
|
|
38
|
+
.popoverWrapper
|
|
39
|
+
.popoverOverlay
|
|
40
|
+
position absolute
|
|
41
|
+
top 0
|
|
42
|
+
left 0
|
|
43
|
+
right 0
|
|
44
|
+
bottom 0
|
|
45
|
+
|
|
46
|
+
+web()
|
|
47
|
+
cursor default
|
|
48
|
+
|
|
49
|
+
.popover
|
|
50
|
+
background-color var(--color-bg-main-strong)
|
|
51
|
+
radius()
|
|
52
|
+
shadow(3)
|
|
53
|
+
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// DO NOT MODIFY THIS FILE - IT IS AUTOMATICALLY GENERATED ON COMMITS.
|
|
3
|
+
|
|
4
|
+
import { type ReactNode, type RefObject } from 'react';
|
|
5
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
6
|
+
import { type UITextInputProps } from '@startupjs-ui/text-input';
|
|
7
|
+
import './index.cssx.styl';
|
|
8
|
+
declare const _default: import("react").ComponentType<DateTimePickerProps>;
|
|
9
|
+
export default _default;
|
|
10
|
+
export declare const _PropsJsonSchema: {};
|
|
11
|
+
export interface DateTimePickerProps extends Omit<UITextInputProps, 'value' | 'onChangeText' | 'ref'> {
|
|
12
|
+
/** Ref to access underlying input */
|
|
13
|
+
ref?: RefObject<any>;
|
|
14
|
+
/** Custom styles applied to the root input */
|
|
15
|
+
style?: StyleProp<ViewStyle>;
|
|
16
|
+
/** Custom styles for content container */
|
|
17
|
+
contentStyle?: Record<string, any>;
|
|
18
|
+
/** Date formatting string from moment */
|
|
19
|
+
dateFormat?: string;
|
|
20
|
+
/** Picker display mode @default 'spinner' */
|
|
21
|
+
display?: 'default' | 'spinner' | 'calendar' | 'clock';
|
|
22
|
+
/** Minutes step for time selection @default 5 */
|
|
23
|
+
timeInterval?: number;
|
|
24
|
+
/** Force 24 hour time format (auto-detected when not provided) */
|
|
25
|
+
is24Hour?: boolean;
|
|
26
|
+
/** Input size preset @default 'm' */
|
|
27
|
+
size?: 'l' | 'm' | 's';
|
|
28
|
+
/** Picker type @default 'datetime' */
|
|
29
|
+
mode?: 'date' | 'time' | 'datetime' | 'countdown';
|
|
30
|
+
/** Custom renderer for input component */
|
|
31
|
+
renderInput?: (inputProps: Record<string, any> & {
|
|
32
|
+
onChangeVisible: (value: boolean) => void;
|
|
33
|
+
}) => ReactNode;
|
|
34
|
+
/** Locale identifier (falls back to device locale) */
|
|
35
|
+
locale?: string;
|
|
36
|
+
/** Selected date range (start/end timestamps) */
|
|
37
|
+
range?: [number, number];
|
|
38
|
+
/** IANA timezone name @default moment.tz.guess() */
|
|
39
|
+
timezone?: string;
|
|
40
|
+
/** Array of disabled day timestamps */
|
|
41
|
+
disabledDays?: number[];
|
|
42
|
+
/** Current value as timestamp */
|
|
43
|
+
date?: number;
|
|
44
|
+
/** Disable interactions @default false */
|
|
45
|
+
disabled?: boolean;
|
|
46
|
+
/** Render as readonly @default false */
|
|
47
|
+
readonly?: boolean;
|
|
48
|
+
/** Placeholder text */
|
|
49
|
+
placeholder?: string;
|
|
50
|
+
/** Maximum available date as timestamp */
|
|
51
|
+
maxDate?: number;
|
|
52
|
+
/** Minimum available date as timestamp */
|
|
53
|
+
minDate?: number;
|
|
54
|
+
/** Controlled visibility */
|
|
55
|
+
visible?: boolean;
|
|
56
|
+
/** Scoped model controlling visibility */
|
|
57
|
+
$visible?: any;
|
|
58
|
+
/** Test identifier for the input */
|
|
59
|
+
testID?: string;
|
|
60
|
+
/** Test identifier for calendar root */
|
|
61
|
+
calendarTestID?: string;
|
|
62
|
+
/** Called when user presses input (native) */
|
|
63
|
+
onPressIn?: (...args: any[]) => void;
|
|
64
|
+
/** Called with selected timestamp (or undefined when cleared) */
|
|
65
|
+
onChangeDate?: (value?: number) => void;
|
|
66
|
+
/** Called before opening (controlled visibility mode) */
|
|
67
|
+
onRequestOpen?: () => void;
|
|
68
|
+
/** Called before closing (controlled visibility mode) */
|
|
69
|
+
onRequestClose?: () => void;
|
|
70
|
+
/** Error state flag @private */
|
|
71
|
+
_hasError?: boolean;
|
|
72
|
+
}
|
package/index.tsx
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useMemo,
|
|
3
|
+
useRef,
|
|
4
|
+
useState,
|
|
5
|
+
useEffect,
|
|
6
|
+
useImperativeHandle,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
type RefObject
|
|
9
|
+
} from 'react'
|
|
10
|
+
import { Platform, type StyleProp, type ViewStyle } from 'react-native'
|
|
11
|
+
import RNDateTimePicker, { DateTimePickerAndroid } from '@react-native-community/datetimepicker'
|
|
12
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
13
|
+
import { pug, observer, useBind, $ } from 'startupjs'
|
|
14
|
+
import { themed, useMedia } from '@startupjs-ui/core'
|
|
15
|
+
import Button from '@startupjs-ui/button'
|
|
16
|
+
import Div from '@startupjs-ui/div'
|
|
17
|
+
import Divider from '@startupjs-ui/divider'
|
|
18
|
+
import TextInput, { type UITextInputProps } from '@startupjs-ui/text-input'
|
|
19
|
+
import AbstractPopover from '@startupjs-ui/abstract-popover'
|
|
20
|
+
import Drawer from '@startupjs-ui/drawer'
|
|
21
|
+
import { faTimesCircle } from '@fortawesome/free-solid-svg-icons/faTimesCircle'
|
|
22
|
+
import moment from 'moment-timezone'
|
|
23
|
+
import { getLocale } from './helpers'
|
|
24
|
+
import { Calendar, TimeSelect } from './components'
|
|
25
|
+
import './index.cssx.styl'
|
|
26
|
+
|
|
27
|
+
export default observer(themed('DateTimePicker', DateTimePicker))
|
|
28
|
+
|
|
29
|
+
export const _PropsJsonSchema = {/* DateTimePickerProps */}
|
|
30
|
+
|
|
31
|
+
export interface DateTimePickerProps extends Omit<UITextInputProps, 'value' | 'onChangeText' | 'ref'> {
|
|
32
|
+
/** Ref to access underlying input */
|
|
33
|
+
ref?: RefObject<any>
|
|
34
|
+
/** Custom styles applied to the root input */
|
|
35
|
+
style?: StyleProp<ViewStyle>
|
|
36
|
+
/** Custom styles for content container */
|
|
37
|
+
contentStyle?: Record<string, any>
|
|
38
|
+
/** Date formatting string from moment */
|
|
39
|
+
dateFormat?: string
|
|
40
|
+
/** Picker display mode @default 'spinner' */
|
|
41
|
+
display?: 'default' | 'spinner' | 'calendar' | 'clock'
|
|
42
|
+
/** Minutes step for time selection @default 5 */
|
|
43
|
+
timeInterval?: number
|
|
44
|
+
/** Force 24 hour time format (auto-detected when not provided) */
|
|
45
|
+
is24Hour?: boolean
|
|
46
|
+
/** Input size preset @default 'm' */
|
|
47
|
+
size?: 'l' | 'm' | 's'
|
|
48
|
+
/** Picker type @default 'datetime' */
|
|
49
|
+
mode?: 'date' | 'time' | 'datetime' | 'countdown'
|
|
50
|
+
/** Custom renderer for input component */
|
|
51
|
+
renderInput?: (inputProps: Record<string, any> & { onChangeVisible: (value: boolean) => void }) => ReactNode
|
|
52
|
+
/** Locale identifier (falls back to device locale) */
|
|
53
|
+
locale?: string
|
|
54
|
+
/** Selected date range (start/end timestamps) */
|
|
55
|
+
range?: [number, number]
|
|
56
|
+
/** IANA timezone name @default moment.tz.guess() */
|
|
57
|
+
timezone?: string
|
|
58
|
+
/** Array of disabled day timestamps */
|
|
59
|
+
disabledDays?: number[]
|
|
60
|
+
/** Current value as timestamp */
|
|
61
|
+
date?: number
|
|
62
|
+
/** Disable interactions @default false */
|
|
63
|
+
disabled?: boolean
|
|
64
|
+
/** Render as readonly @default false */
|
|
65
|
+
readonly?: boolean
|
|
66
|
+
/** Placeholder text */
|
|
67
|
+
placeholder?: string
|
|
68
|
+
/** Maximum available date as timestamp */
|
|
69
|
+
maxDate?: number
|
|
70
|
+
/** Minimum available date as timestamp */
|
|
71
|
+
minDate?: number
|
|
72
|
+
/** Controlled visibility */
|
|
73
|
+
visible?: boolean
|
|
74
|
+
/** Scoped model controlling visibility */
|
|
75
|
+
$visible?: any
|
|
76
|
+
/** Test identifier for the input */
|
|
77
|
+
testID?: string
|
|
78
|
+
/** Test identifier for calendar root */
|
|
79
|
+
calendarTestID?: string
|
|
80
|
+
/** Called when user presses input (native) */
|
|
81
|
+
onPressIn?: (...args: any[]) => void
|
|
82
|
+
/** Called with selected timestamp (or undefined when cleared) */
|
|
83
|
+
onChangeDate?: (value?: number) => void
|
|
84
|
+
/** Called before opening (controlled visibility mode) */
|
|
85
|
+
onRequestOpen?: () => void
|
|
86
|
+
/** Called before closing (controlled visibility mode) */
|
|
87
|
+
onRequestClose?: () => void
|
|
88
|
+
/** Error state flag @private */
|
|
89
|
+
_hasError?: boolean
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function DateTimePicker ({
|
|
93
|
+
style,
|
|
94
|
+
contentStyle = {},
|
|
95
|
+
dateFormat,
|
|
96
|
+
display = 'spinner',
|
|
97
|
+
timeInterval = 5,
|
|
98
|
+
is24Hour,
|
|
99
|
+
size = 'm',
|
|
100
|
+
mode = 'datetime',
|
|
101
|
+
renderInput,
|
|
102
|
+
locale,
|
|
103
|
+
range,
|
|
104
|
+
timezone = moment.tz.guess(),
|
|
105
|
+
disabledDays = [],
|
|
106
|
+
date,
|
|
107
|
+
disabled,
|
|
108
|
+
readonly,
|
|
109
|
+
placeholder,
|
|
110
|
+
maxDate,
|
|
111
|
+
minDate,
|
|
112
|
+
visible,
|
|
113
|
+
$visible,
|
|
114
|
+
testID,
|
|
115
|
+
calendarTestID,
|
|
116
|
+
onPressIn,
|
|
117
|
+
onChangeDate,
|
|
118
|
+
onRequestOpen,
|
|
119
|
+
onRequestClose,
|
|
120
|
+
_hasError,
|
|
121
|
+
ref,
|
|
122
|
+
...props
|
|
123
|
+
}: DateTimePickerProps): ReactNode {
|
|
124
|
+
const media: any = useMedia()
|
|
125
|
+
const [textInput, setTextInput] = useState('')
|
|
126
|
+
const refTimeSelect = useRef<any>(null)
|
|
127
|
+
const inputRef = useRef<any>(null)
|
|
128
|
+
const insets = useSafeAreaInsets()
|
|
129
|
+
|
|
130
|
+
useImperativeHandle(ref, () => inputRef.current)
|
|
131
|
+
|
|
132
|
+
function onDismiss () {
|
|
133
|
+
onChangeVisible(false)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getTimestampFromValue (value: any) {
|
|
137
|
+
if (value?.nativeEvent?.timestamp) {
|
|
138
|
+
return value.nativeEvent.timestamp
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (value instanceof Date) {
|
|
142
|
+
return value.getTime()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return value
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function showAndroidPicker () {
|
|
149
|
+
const showTimepicker = (selectedDate: Date) => {
|
|
150
|
+
;(DateTimePickerAndroid as any).open({
|
|
151
|
+
value: selectedDate,
|
|
152
|
+
mode: 'time',
|
|
153
|
+
display: 'time',
|
|
154
|
+
is24Hour,
|
|
155
|
+
onChange: (event: any, selectedTime: Date) => {
|
|
156
|
+
if (event.type === 'set') {
|
|
157
|
+
const finalDate = new Date(selectedDate)
|
|
158
|
+
finalDate.setHours(selectedTime.getHours())
|
|
159
|
+
finalDate.setMinutes(selectedTime.getMinutes())
|
|
160
|
+
_onChangeDate(finalDate)
|
|
161
|
+
} else {
|
|
162
|
+
onDismiss()
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const showDatepicker = () => {
|
|
169
|
+
;(DateTimePickerAndroid as any).open({
|
|
170
|
+
value: tempDate,
|
|
171
|
+
mode: 'date',
|
|
172
|
+
display: 'calendar',
|
|
173
|
+
maximumDate: maxDate ? new Date(maxDate) : undefined,
|
|
174
|
+
minimumDate: minDate ? new Date(minDate) : undefined,
|
|
175
|
+
onChange: (event: any, selectedDate: Date) => {
|
|
176
|
+
if (event.type === 'set') {
|
|
177
|
+
if (mode === 'datetime') {
|
|
178
|
+
showTimepicker(selectedDate)
|
|
179
|
+
} else {
|
|
180
|
+
_onChangeDate(selectedDate)
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
onDismiss()
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
switch (mode) {
|
|
190
|
+
case 'date':
|
|
191
|
+
showDatepicker()
|
|
192
|
+
break
|
|
193
|
+
case 'time':
|
|
194
|
+
;(DateTimePickerAndroid as any).open({
|
|
195
|
+
value: tempDate,
|
|
196
|
+
mode: 'time',
|
|
197
|
+
display: 'clock',
|
|
198
|
+
is24Hour,
|
|
199
|
+
onChange: (event: any, selectedTime: Date) => {
|
|
200
|
+
if (event.type === 'set') {
|
|
201
|
+
_onChangeDate(selectedTime)
|
|
202
|
+
} else {
|
|
203
|
+
onDismiss()
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
break
|
|
208
|
+
case 'datetime':
|
|
209
|
+
showDatepicker()
|
|
210
|
+
break
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let bindProps: any = useMemo(() => {
|
|
215
|
+
if (typeof $visible !== 'undefined') return { $visible }
|
|
216
|
+
if (typeof onRequestOpen === 'function' && typeof onRequestClose === 'function') {
|
|
217
|
+
return {
|
|
218
|
+
visible,
|
|
219
|
+
onChangeVisible: (value: boolean) => {
|
|
220
|
+
if (value) {
|
|
221
|
+
onRequestOpen()
|
|
222
|
+
} else {
|
|
223
|
+
onRequestClose()
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (Platform.OS === 'android' && value) {
|
|
227
|
+
showAndroidPicker()
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
233
|
+
|
|
234
|
+
if (!bindProps) {
|
|
235
|
+
$visible = $(false)
|
|
236
|
+
bindProps = { $visible }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let { onChangeVisible } = bindProps
|
|
240
|
+
;({ visible, onChangeVisible } = useBind({ $visible, visible, onChangeVisible }) as any)
|
|
241
|
+
|
|
242
|
+
const [tempDate, setTempDate] = useTempDate({ visible: !!visible, date, timezone })
|
|
243
|
+
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
// Prevent crashes when custom renderer passed via props
|
|
246
|
+
if (renderInput) return
|
|
247
|
+
if (visible) {
|
|
248
|
+
inputRef?.current?.focus?.()
|
|
249
|
+
} else {
|
|
250
|
+
inputRef?.current?.blur?.()
|
|
251
|
+
}
|
|
252
|
+
}, [visible, renderInput])
|
|
253
|
+
|
|
254
|
+
const exactLocale = useMemo(() => {
|
|
255
|
+
const locales = moment.locales()
|
|
256
|
+
const _locale = locale ?? getLocale()
|
|
257
|
+
return locales.includes(_locale) ? _locale : 'en'
|
|
258
|
+
}, [locale])
|
|
259
|
+
|
|
260
|
+
const _dateFormat = useMemo(() => {
|
|
261
|
+
if (dateFormat) return dateFormat
|
|
262
|
+
if (mode === 'datetime') {
|
|
263
|
+
return (moment().locale(exactLocale) as any)._locale._longDateFormat.L + ' ' +
|
|
264
|
+
(moment().locale(exactLocale) as any)._locale._longDateFormat.LT
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (mode === 'date') return (moment().locale(exactLocale) as any)._locale._longDateFormat.L
|
|
268
|
+
if (mode === 'time') return (moment().locale(exactLocale) as any)._locale._longDateFormat.LT
|
|
269
|
+
}, [mode, dateFormat, exactLocale])
|
|
270
|
+
|
|
271
|
+
useEffect(() => {
|
|
272
|
+
if (typeof date === 'undefined') {
|
|
273
|
+
setTextInput('')
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const value = +moment.tz(date, timezone).seconds(0).milliseconds(0)
|
|
278
|
+
setTextInput(moment.tz(value, timezone).format(_dateFormat))
|
|
279
|
+
setTempDate(new Date(value))
|
|
280
|
+
}, [date, timezone, _dateFormat, setTempDate])
|
|
281
|
+
|
|
282
|
+
function _onChangeDate (value: any) {
|
|
283
|
+
const timestamp = getTimestampFromValue(value)
|
|
284
|
+
onChangeDate && onChangeDate(timestamp)
|
|
285
|
+
onChangeVisible(false)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function _onPressIn (...args: any[]) {
|
|
289
|
+
if (Platform.OS === 'android') {
|
|
290
|
+
showAndroidPicker()
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
onChangeVisible(true)
|
|
294
|
+
|
|
295
|
+
onPressIn && onPressIn(...args)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const inputProps: Record<string, any> = {
|
|
299
|
+
style,
|
|
300
|
+
ref: inputRef,
|
|
301
|
+
disabled,
|
|
302
|
+
readonly,
|
|
303
|
+
size,
|
|
304
|
+
placeholder,
|
|
305
|
+
_hasError,
|
|
306
|
+
value: textInput,
|
|
307
|
+
testID,
|
|
308
|
+
...props
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (Platform.OS === 'web') {
|
|
312
|
+
inputProps.editable = false
|
|
313
|
+
inputProps.onFocus = _onPressIn
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (Platform.OS === 'android') {
|
|
317
|
+
inputProps.onFocus = _onPressIn
|
|
318
|
+
// Hide cursor for android
|
|
319
|
+
inputProps.cursorColor = 'transparent'
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (Platform.OS === 'ios') {
|
|
323
|
+
inputProps.onPressIn = _onPressIn
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function handleRenderedInputPress (value: boolean) {
|
|
327
|
+
if (Platform.OS === 'android' && value) {
|
|
328
|
+
showAndroidPicker()
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
onChangeVisible(value)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const caption = pug`
|
|
335
|
+
if renderInput
|
|
336
|
+
= renderInput(Object.assign({ onChangeVisible: handleRenderedInputPress }, inputProps))
|
|
337
|
+
else
|
|
338
|
+
TextInput(
|
|
339
|
+
showSoftInputOnFocus=false
|
|
340
|
+
secondaryIcon=textInput && !renderInput ? faTimesCircle : undefined,
|
|
341
|
+
onSecondaryIconPress=() => onChangeDate && onChangeDate()
|
|
342
|
+
...inputProps
|
|
343
|
+
)
|
|
344
|
+
`
|
|
345
|
+
|
|
346
|
+
function renderPopoverContent (): ReactNode {
|
|
347
|
+
return pug`
|
|
348
|
+
Div.content(
|
|
349
|
+
style={
|
|
350
|
+
paddingBottom: (media.tablet && Platform.OS === 'ios') ? 0 : insets.bottom,
|
|
351
|
+
...contentStyle
|
|
352
|
+
}
|
|
353
|
+
)
|
|
354
|
+
if Platform.OS === 'web'
|
|
355
|
+
if (mode === 'date') || (mode === 'datetime')
|
|
356
|
+
Calendar(
|
|
357
|
+
date=tempDate
|
|
358
|
+
exactLocale=exactLocale
|
|
359
|
+
disabledDays=disabledDays
|
|
360
|
+
locale=locale
|
|
361
|
+
maxDate=maxDate
|
|
362
|
+
minDate=minDate
|
|
363
|
+
range=range
|
|
364
|
+
timezone=timezone
|
|
365
|
+
testID=calendarTestID
|
|
366
|
+
onChangeDate=(newDate) => {
|
|
367
|
+
setTempDate(newDate)
|
|
368
|
+
if (mode === 'date') _onChangeDate(newDate)
|
|
369
|
+
}
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
if mode === 'datetime'
|
|
373
|
+
Divider.divider
|
|
374
|
+
|
|
375
|
+
if (mode === 'time') || (mode === 'datetime')
|
|
376
|
+
TimeSelect(
|
|
377
|
+
date=tempDate
|
|
378
|
+
ref=refTimeSelect
|
|
379
|
+
maxDate=maxDate
|
|
380
|
+
minDate=minDate
|
|
381
|
+
timezone=timezone
|
|
382
|
+
exactLocale=exactLocale
|
|
383
|
+
is24Hour=is24Hour
|
|
384
|
+
timeInterval=timeInterval
|
|
385
|
+
onChangeDate=(newTime) => {
|
|
386
|
+
const finalDate = new Date(tempDate)
|
|
387
|
+
finalDate.setHours(new Date(newTime).getHours())
|
|
388
|
+
finalDate.setMinutes(new Date(newTime).getMinutes())
|
|
389
|
+
_onChangeDate(finalDate)
|
|
390
|
+
}
|
|
391
|
+
)
|
|
392
|
+
else if Platform.OS === 'ios'
|
|
393
|
+
Div.actions(row)
|
|
394
|
+
Button(
|
|
395
|
+
size='s'
|
|
396
|
+
color='secondary'
|
|
397
|
+
variant='text'
|
|
398
|
+
onPress=() => {
|
|
399
|
+
onDismiss()
|
|
400
|
+
}
|
|
401
|
+
) Cancel
|
|
402
|
+
Button(
|
|
403
|
+
size='s'
|
|
404
|
+
color='primary'
|
|
405
|
+
variant='text'
|
|
406
|
+
onPress=() => {
|
|
407
|
+
_onChangeDate(tempDate)
|
|
408
|
+
}
|
|
409
|
+
) Done
|
|
410
|
+
RNDateTimePicker.rnPicker(
|
|
411
|
+
value=tempDate
|
|
412
|
+
display=display
|
|
413
|
+
is24Hour=is24Hour
|
|
414
|
+
disabled=disabled
|
|
415
|
+
mode=mode
|
|
416
|
+
themeVariant='light'
|
|
417
|
+
textColor='#000000cc'
|
|
418
|
+
maximumDate=maxDate ? new Date(maxDate) : undefined
|
|
419
|
+
minimumDate=minDate ? new Date(minDate) : undefined
|
|
420
|
+
timeZoneName=timezone
|
|
421
|
+
onChange=(event, selectedDate) => {
|
|
422
|
+
if (event.type !== 'dismissed') {
|
|
423
|
+
setTempDate(selectedDate)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
)
|
|
427
|
+
`
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function renderWrapper (children: ReactNode): ReactNode {
|
|
431
|
+
return pug`
|
|
432
|
+
Div.popoverWrapper
|
|
433
|
+
Div.popoverOverlay(feedback=false onPress=()=> onChangeVisible(false))
|
|
434
|
+
= children
|
|
435
|
+
`
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return pug`
|
|
439
|
+
// Android datetimepicker rendered inside its own modal
|
|
440
|
+
if Platform.OS === 'android'
|
|
441
|
+
= caption
|
|
442
|
+
else
|
|
443
|
+
if media.tablet
|
|
444
|
+
= caption
|
|
445
|
+
AbstractPopover.popover(
|
|
446
|
+
visible=visible
|
|
447
|
+
anchorRef=inputRef
|
|
448
|
+
renderWrapper=renderWrapper
|
|
449
|
+
)= renderPopoverContent()
|
|
450
|
+
else
|
|
451
|
+
= caption
|
|
452
|
+
Drawer.drawer(
|
|
453
|
+
visible=visible
|
|
454
|
+
position='bottom'
|
|
455
|
+
swipeStyleName='swipe'
|
|
456
|
+
AreaComponent=Div
|
|
457
|
+
onDismiss=onDismiss
|
|
458
|
+
)= renderPopoverContent()
|
|
459
|
+
`
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function useTempDate ({
|
|
463
|
+
visible,
|
|
464
|
+
date,
|
|
465
|
+
timezone
|
|
466
|
+
}: {
|
|
467
|
+
visible: boolean
|
|
468
|
+
date?: number
|
|
469
|
+
timezone: string
|
|
470
|
+
}) {
|
|
471
|
+
const [tempDate, setTempDate] = useState(getTempDate(date, timezone))
|
|
472
|
+
|
|
473
|
+
useEffect(() => {
|
|
474
|
+
const tempDate = getTempDate(date, timezone)
|
|
475
|
+
setTempDate(tempDate)
|
|
476
|
+
}, [visible, date, timezone])
|
|
477
|
+
|
|
478
|
+
return [tempDate, setTempDate] as const
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function getTempDate (date: number | undefined, timezone: string) {
|
|
482
|
+
return date
|
|
483
|
+
? new Date(+moment.tz(date, timezone).seconds(0).milliseconds(0))
|
|
484
|
+
: new Date()
|
|
485
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@startupjs-ui/date-time-picker",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"main": "index.tsx",
|
|
8
|
+
"types": "index.d.ts",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@fortawesome/free-solid-svg-icons": "^5.12.0",
|
|
12
|
+
"@react-native-community/datetimepicker": "^8.4.4",
|
|
13
|
+
"@startupjs-ui/abstract-popover": "^0.1.3",
|
|
14
|
+
"@startupjs-ui/button": "^0.1.3",
|
|
15
|
+
"@startupjs-ui/core": "^0.1.3",
|
|
16
|
+
"@startupjs-ui/div": "^0.1.3",
|
|
17
|
+
"@startupjs-ui/divider": "^0.1.3",
|
|
18
|
+
"@startupjs-ui/drawer": "^0.1.3",
|
|
19
|
+
"@startupjs-ui/flat-list": "^0.1.3",
|
|
20
|
+
"@startupjs-ui/icon": "^0.1.3",
|
|
21
|
+
"@startupjs-ui/popover": "^0.1.3",
|
|
22
|
+
"@startupjs-ui/span": "^0.1.3",
|
|
23
|
+
"@startupjs-ui/text-input": "^0.1.3",
|
|
24
|
+
"moment-timezone": "^0.5.31",
|
|
25
|
+
"react-native-safe-area-context": "^5.6.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"react": "*",
|
|
29
|
+
"react-native": "*",
|
|
30
|
+
"startupjs": "*"
|
|
31
|
+
},
|
|
32
|
+
"gitHead": "fd964ebc3892d3dd0a6c85438c0af619cc50c3f0"
|
|
33
|
+
}
|