@xy-planning-network/trees 0.13.12 → 0.13.13
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/dist/trees.css +1 -1
- package/dist/trees.es.js +11229 -6547
- package/dist/trees.umd.js +20 -22
- package/package.json +2 -2
- package/src/index.css +61 -3
- package/src/lib-components/forms/DateRangePicker.vue +178 -129
- package/src/lib-components/forms/RangeCalendar.vue +245 -0
- package/src/lib-components/lists/DynamicTable.vue +3 -2
- package/types/composables/date.d.ts +4 -4
- package/types/composables/dateRange.d.ts +16 -0
- package/types/composables/forms.d.ts +9 -2
- package/types/composables/index.d.ts +2 -0
- package/types/lib-components/forms/DateRangePicker.vue.d.ts +73 -8
- package/types/lib-components/forms/RangeCalendar.vue.d.ts +34 -0
- package/types/lib-components/index.d.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xy-planning-network/trees",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.13",
|
|
4
4
|
"description": "",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "github:xy-planning-network/trees",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"@maskito/kit": "^3.5.0",
|
|
65
65
|
"@maskito/vue": "^3.5.0",
|
|
66
66
|
"axios": "^1.5.0",
|
|
67
|
-
"
|
|
67
|
+
"reka-ui": "^2.9.9"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
70
|
"@tailwindcss/forms": "^0.5.2",
|
package/src/index.css
CHANGED
|
@@ -229,9 +229,67 @@
|
|
|
229
229
|
@apply font-medium border-b-2 border-b-xy-blue hover:border-b-transparent hover:text-xy-blue;
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
/*
|
|
233
|
-
|
|
234
|
-
|
|
232
|
+
/* RangeCalendar */
|
|
233
|
+
|
|
234
|
+
/* Calendar Prev/Next navigation */
|
|
235
|
+
.xy-range-cal-nav[data-disabled] {
|
|
236
|
+
@apply cursor-not-allowed opacity-40 hover:bg-transparent;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* Calendar Day Trigger */
|
|
240
|
+
.xy-range-cal-trigger {
|
|
241
|
+
@apply relative flex items-center justify-center rounded-full whitespace-nowrap text-sm font-normal text-black w-8 h-8 outline-none;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.xy-range-cal-trigger:focus-visible {
|
|
245
|
+
@apply shadow-[0_0_0_2px] shadow-xy-blue-500;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.xy-range-cal-trigger[data-highlighted] {
|
|
249
|
+
@apply bg-neutral-100;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.xy-range-cal-trigger[data-outside-view] {
|
|
253
|
+
@apply text-black/50;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.xy-range-cal-trigger[data-unavailable] {
|
|
257
|
+
@apply pointer-events-none text-black/50 line-through;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.xy-range-cal-trigger[data-disabled] {
|
|
261
|
+
@apply cursor-not-allowed text-black/50;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.xy-range-cal-trigger[data-selected] {
|
|
265
|
+
@apply !bg-xy-blue-700 text-white;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.xy-range-cal-trigger:hover {
|
|
269
|
+
@apply bg-xy-blue-700 text-white;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.xy-range-cal-trigger[data-disabled]:hover,
|
|
273
|
+
.xy-range-cal-trigger[data-unavailable]:hover {
|
|
274
|
+
@apply bg-transparent text-black/50;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/* Today Indicator Dot */
|
|
278
|
+
.xy-range-cal-trigger::before {
|
|
279
|
+
@apply absolute bottom-[3px] hidden h-1 w-1 rounded-full bg-white;
|
|
280
|
+
content: "";
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.xy-range-cal-trigger[data-today]::before {
|
|
284
|
+
@apply block bg-xy-blue-600;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.xy-range-cal-trigger[data-today][data-selected]::before,
|
|
288
|
+
.xy-range-cal-trigger[data-today][data-selection-start]::before,
|
|
289
|
+
.xy-range-cal-trigger[data-today][data-selection-end]::before,
|
|
290
|
+
.xy-range-cal-trigger[data-today][data-highlighted-start]::before,
|
|
291
|
+
.xy-range-cal-trigger[data-today][data-highlighted-end]::before {
|
|
292
|
+
@apply bg-white;
|
|
235
293
|
}
|
|
236
294
|
}
|
|
237
295
|
|
|
@@ -1,166 +1,215 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import
|
|
3
|
-
import InputHelp from "./InputHelp.vue"
|
|
4
|
-
import InputError from "./InputError.vue"
|
|
5
|
-
import flatpickr from "flatpickr"
|
|
6
|
-
import "flatpickr/dist/flatpickr.min.css"
|
|
7
|
-
import { onMounted, useTemplateRef, watch } from "vue"
|
|
2
|
+
import { computed, nextTick, ref, useTemplateRef } from "vue"
|
|
8
3
|
import {
|
|
9
4
|
defaultInputProps,
|
|
10
5
|
defaultModelOpts,
|
|
11
6
|
useInputField,
|
|
12
7
|
} from "@/composables/forms"
|
|
13
8
|
import type { DateRangeInput } from "@/composables/forms"
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
9
|
+
import RangeCalendar from "@/lib-components/forms/RangeCalendar.vue"
|
|
10
|
+
import { CalendarDateRangeIcon, XMarkIcon } from "@heroicons/vue/solid"
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
PopoverAnchor,
|
|
14
|
+
PopoverContent,
|
|
15
|
+
PopoverPortal,
|
|
16
|
+
PopoverRoot,
|
|
17
|
+
PopoverTrigger,
|
|
18
|
+
} from "reka-ui"
|
|
19
|
+
import InputHelp from "@/lib-components/forms/InputHelp.vue"
|
|
20
|
+
import InputError from "@/lib-components/forms/InputError.vue"
|
|
21
|
+
import InputLabel from "@/lib-components/forms/InputLabel.vue"
|
|
16
22
|
|
|
17
23
|
defineOptions({
|
|
18
24
|
inheritAttrs: false,
|
|
19
25
|
})
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
/**
|
|
28
|
+
* NOTE(spk): Default actions made available by Trees do not
|
|
29
|
+
* account for minValue, maxValue, and maxRange boundary restrictions.
|
|
30
|
+
*
|
|
31
|
+
* Likewise a pre-hydrated v-model that is outside those bounds also does
|
|
32
|
+
* not apply boundaries to that initial value.
|
|
33
|
+
*/
|
|
24
34
|
const props = withDefaults(defineProps<DateRangeInput>(), {
|
|
25
35
|
...defaultInputProps,
|
|
26
|
-
|
|
27
|
-
maxRange:
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
actions: () => [],
|
|
37
|
+
maxRange: undefined,
|
|
38
|
+
maxValue: () => new Date(),
|
|
39
|
+
minValue: undefined,
|
|
40
|
+
placeholder: "mm-dd-yyyy to mm-dd-yyyy",
|
|
41
|
+
position: "bottom-start",
|
|
30
42
|
})
|
|
31
43
|
|
|
32
44
|
const modelState = defineModel<DateRangeInput["modelValue"]>({
|
|
33
45
|
...defaultModelOpts,
|
|
34
|
-
default: { maxDate:
|
|
46
|
+
default: { maxDate: undefined, minDate: undefined },
|
|
35
47
|
})
|
|
36
48
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
const {
|
|
50
|
+
aria,
|
|
51
|
+
errorState,
|
|
52
|
+
inputID,
|
|
53
|
+
isDisabled,
|
|
54
|
+
isRequired,
|
|
55
|
+
nameAttr,
|
|
56
|
+
onInvalid,
|
|
57
|
+
} = useInputField(props)
|
|
58
|
+
|
|
59
|
+
const dateFormatter = new Intl.DateTimeFormat("en-US", {
|
|
60
|
+
year: "numeric",
|
|
61
|
+
month: "short",
|
|
62
|
+
day: "2-digit",
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const display = computed(() => {
|
|
66
|
+
let out = ""
|
|
45
67
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
// This keeps the change handling scoped to a change triggered by
|
|
51
|
-
// the parent component on v-model.
|
|
52
|
-
const watcher = watch(modelState, () => {
|
|
53
|
-
if (isValidPickerRange(modelState.value)) {
|
|
54
|
-
picker?.setDate(
|
|
55
|
-
[modelState.value.minDate * 1000, modelState.value.maxDate * 1000],
|
|
56
|
-
false
|
|
57
|
-
)
|
|
58
|
-
return
|
|
68
|
+
if (modelState.value?.minDate) {
|
|
69
|
+
out += `${dateFormatter.format(
|
|
70
|
+
new Date(modelState.value.minDate * 1000)
|
|
71
|
+
)} to `
|
|
59
72
|
}
|
|
60
73
|
|
|
61
|
-
|
|
74
|
+
if (modelState.value?.maxDate) {
|
|
75
|
+
out += dateFormatter.format(new Date(modelState.value.maxDate * 1000))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return out
|
|
62
79
|
})
|
|
63
80
|
|
|
64
|
-
const
|
|
65
|
-
|
|
81
|
+
const reset = () => {
|
|
82
|
+
modelState.value = { maxDate: 0, minDate: 0 }
|
|
83
|
+
}
|
|
66
84
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
85
|
+
const isValid = computed(() => {
|
|
86
|
+
return !!(modelState.value?.maxDate && modelState.value?.minDate)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const onUpdate = () => {
|
|
90
|
+
nextTick(() => {
|
|
91
|
+
if (isValid.value) {
|
|
92
|
+
errorState.value = ""
|
|
93
|
+
isOpen.value = false
|
|
94
|
+
}
|
|
95
|
+
})
|
|
71
96
|
}
|
|
72
97
|
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
onMounted(() => {
|
|
78
|
-
const opts: flatpickr.Options.Options = {
|
|
79
|
-
allowInput: !props.maxRange,
|
|
80
|
-
appendTo: wrapperRef.value || undefined,
|
|
81
|
-
dateFormat: "m-d-Y",
|
|
82
|
-
mode: "range",
|
|
83
|
-
maxDate: props.maxDate,
|
|
84
|
-
minDate: props.startDate,
|
|
85
|
-
onClose: (selectedDates) => {
|
|
86
|
-
if (selectedDates.length === 2) {
|
|
87
|
-
updateModelState({
|
|
88
|
-
minDate: selectedDates[0].setUTCHours(0, 0, 0, 0) / 1000,
|
|
89
|
-
maxDate: Math.floor(
|
|
90
|
-
selectedDates[1].setUTCHours(23, 59, 59, 999) / 1000
|
|
91
|
-
),
|
|
92
|
-
})
|
|
93
|
-
} else if (selectedDates.length === 0) {
|
|
94
|
-
updateModelState({
|
|
95
|
-
minDate: 0,
|
|
96
|
-
maxDate: 0,
|
|
97
|
-
})
|
|
98
|
-
}
|
|
99
|
-
},
|
|
100
|
-
static: true,
|
|
98
|
+
const isOpen = ref(false)
|
|
99
|
+
const onUpdateIsOpen = (open: boolean) => {
|
|
100
|
+
if (!open && !isValid.value) {
|
|
101
|
+
reset()
|
|
101
102
|
}
|
|
103
|
+
}
|
|
102
104
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
modelState.value.minDate * 1000,
|
|
107
|
-
modelState.value.maxDate * 1000,
|
|
108
|
-
]
|
|
109
|
-
}
|
|
105
|
+
const calendarRef = useTemplateRef<InstanceType<typeof RangeCalendar> | null>(
|
|
106
|
+
"calendar"
|
|
107
|
+
)
|
|
110
108
|
|
|
111
|
-
|
|
112
|
-
// Handle onChange to dynamically adjust maxDate to x days ahead of the selected start date
|
|
113
|
-
opts.onChange = (selectedDates, _, self) => {
|
|
114
|
-
if (selectedDates.length === 1) {
|
|
115
|
-
// Clone date so as to not change selectedDates[0] value
|
|
116
|
-
var daysAhead = new Date(selectedDates[0].getTime())
|
|
117
|
-
var daysBefore = new Date(selectedDates[0].getTime())
|
|
118
|
-
daysAhead.setDate(daysAhead.getDate() + props.maxRange)
|
|
119
|
-
daysBefore.setDate(daysBefore.getDate() - props.maxRange)
|
|
120
|
-
const now = new Date()
|
|
121
|
-
|
|
122
|
-
if (daysAhead > now) {
|
|
123
|
-
daysAhead = now
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
self.set("minDate", daysBefore)
|
|
127
|
-
self.set("maxDate", daysAhead)
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
109
|
+
const inputRef = useTemplateRef<HTMLInputElement | null>("input")
|
|
131
110
|
|
|
132
|
-
|
|
111
|
+
defineExpose({ calendar: calendarRef, input: inputRef })
|
|
112
|
+
|
|
113
|
+
const alignment = computed(() => {
|
|
114
|
+
switch (props.position) {
|
|
115
|
+
case "bottom-start":
|
|
116
|
+
return "start"
|
|
117
|
+
case "bottom-end":
|
|
118
|
+
return "end"
|
|
119
|
+
default: // bottom
|
|
120
|
+
return "center"
|
|
121
|
+
}
|
|
133
122
|
})
|
|
134
123
|
</script>
|
|
135
124
|
|
|
136
125
|
<template>
|
|
137
|
-
<
|
|
138
|
-
<
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
126
|
+
<PopoverRoot v-model:open="isOpen" @update:open="onUpdateIsOpen">
|
|
127
|
+
<div class="xy-date-range-picker">
|
|
128
|
+
<InputLabel
|
|
129
|
+
:id="aria.labelledby"
|
|
130
|
+
class="mb-2"
|
|
131
|
+
:for="inputID"
|
|
132
|
+
:label="label"
|
|
133
|
+
:required="isRequired"
|
|
134
|
+
/>
|
|
135
|
+
<PopoverAnchor as="div" class="relative">
|
|
136
|
+
<input
|
|
137
|
+
:id="inputID"
|
|
138
|
+
ref="input"
|
|
139
|
+
:aria-labelledby="aria.labelledby"
|
|
140
|
+
:aria-describedby="aria.describedby"
|
|
141
|
+
:aria-errormessage="aria.errormessage"
|
|
142
|
+
:class="[
|
|
143
|
+
'block w-full rounded-md border-0 py-2 pr-9 shadow-sm ring-1 ring-inset focus:ring-2 data-[state=open]:ring-2 sm:text-sm sm:leading-6',
|
|
144
|
+
'disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-700 disabled:ring-gray-200',
|
|
145
|
+
errorState
|
|
146
|
+
? 'text-red-900 ring-red-700 placeholder:text-red-300 focus:ring-red-700 data-[state=open]:ring-red-700'
|
|
147
|
+
: 'text-gray-900 ring-gray-300 placeholder:text-gray-400 focus:ring-xy-blue-500 data-[state=open]:ring-xy-blue-500',
|
|
148
|
+
]"
|
|
149
|
+
:data-state="isOpen ? 'open' : 'closed'"
|
|
150
|
+
:placeholder="placeholder"
|
|
151
|
+
readonly
|
|
152
|
+
:value="display"
|
|
153
|
+
v-bind="$attrs"
|
|
154
|
+
type="text"
|
|
155
|
+
@focus="isOpen = true"
|
|
156
|
+
/>
|
|
157
|
+
|
|
158
|
+
<div class="absolute right-0 top-1/2 -translate-y-1/2 text-neutral-400">
|
|
159
|
+
<button v-if="isValid && !isDisabled" class="p-2" @click="reset">
|
|
160
|
+
<XMarkIcon class="w-5 h-5" />
|
|
161
|
+
</button>
|
|
162
|
+
|
|
163
|
+
<PopoverTrigger class="p-2" :disabled="isDisabled" tabindex="-1">
|
|
164
|
+
<CalendarDateRangeIcon class="w-5 h-5" />
|
|
165
|
+
</PopoverTrigger>
|
|
166
|
+
</div>
|
|
167
|
+
</PopoverAnchor>
|
|
168
|
+
|
|
169
|
+
<InputHelp :id="aria.describedby" class="mt-1" :text="help" />
|
|
170
|
+
<InputError :id="aria.errormessage" class="mt-0.5" :text="errorState" />
|
|
171
|
+
|
|
172
|
+
<input
|
|
173
|
+
class="sr-only top-1 left-1"
|
|
174
|
+
aria-hidden="true"
|
|
175
|
+
:name="`${nameAttr}[minDate]`"
|
|
176
|
+
:required="isRequired"
|
|
177
|
+
:value="modelState?.minDate"
|
|
178
|
+
tabindex="-1"
|
|
179
|
+
@invalid="onInvalid"
|
|
180
|
+
/>
|
|
181
|
+
<input
|
|
182
|
+
class="sr-only top-1 left-1"
|
|
183
|
+
aria-hidden="true"
|
|
184
|
+
:name="`${nameAttr}[maxDate]`"
|
|
185
|
+
:required="isRequired"
|
|
186
|
+
:value="modelState?.maxDate"
|
|
187
|
+
tabindex="-1"
|
|
188
|
+
@invalid="onInvalid"
|
|
189
|
+
/>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<PopoverPortal disabled>
|
|
193
|
+
<PopoverContent
|
|
194
|
+
:align="alignment"
|
|
195
|
+
:align-flip="false"
|
|
196
|
+
:arrow-padding="10"
|
|
197
|
+
class="border border-neutral-100 overflow-hidden rounded-xy-lg shadow z-50"
|
|
198
|
+
side="bottom"
|
|
199
|
+
:side-flip="false"
|
|
200
|
+
:side-offset="5"
|
|
201
|
+
>
|
|
202
|
+
<RangeCalendar
|
|
203
|
+
ref="calendar"
|
|
204
|
+
v-model="modelState"
|
|
205
|
+
:actions="actions"
|
|
206
|
+
borderless
|
|
207
|
+
:max-range="maxRange"
|
|
208
|
+
:max-value="maxValue"
|
|
209
|
+
:min-value="minValue"
|
|
210
|
+
@update:model-value="onUpdate"
|
|
211
|
+
/>
|
|
212
|
+
</PopoverContent>
|
|
213
|
+
</PopoverPortal>
|
|
214
|
+
</PopoverRoot>
|
|
166
215
|
</template>
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { DateRange as RekaDateRange } from "reka-ui"
|
|
3
|
+
import { getLocalTimeZone, CalendarDate } from "@internationalized/date"
|
|
4
|
+
import {
|
|
5
|
+
RangeCalendarCell,
|
|
6
|
+
RangeCalendarCellTrigger,
|
|
7
|
+
RangeCalendarGrid,
|
|
8
|
+
RangeCalendarGridBody,
|
|
9
|
+
RangeCalendarGridHead,
|
|
10
|
+
RangeCalendarGridRow,
|
|
11
|
+
RangeCalendarHeadCell,
|
|
12
|
+
RangeCalendarNext,
|
|
13
|
+
RangeCalendarPrev,
|
|
14
|
+
RangeCalendarRoot,
|
|
15
|
+
useDateFormatter,
|
|
16
|
+
} from "reka-ui"
|
|
17
|
+
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/vue/solid"
|
|
18
|
+
import { computed } from "vue"
|
|
19
|
+
import { defaultModelOpts } from "@/composables/forms"
|
|
20
|
+
import type { DateRangeAction, DateRangeInput } from "@/composables/forms"
|
|
21
|
+
import { calendarDateToUnix } from "@/composables/dateRange"
|
|
22
|
+
|
|
23
|
+
const formatter = useDateFormatter("en-US")
|
|
24
|
+
|
|
25
|
+
defineOptions({
|
|
26
|
+
inheritAttrs: false,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* FIXME(spk): An oddity of Reka-UI RangeCalendar is that is applies
|
|
31
|
+
* [data-highlighted] attributes to all cells inside the range applicable
|
|
32
|
+
* to the maximum-days range, even those that are outside of min-value/max-value.
|
|
33
|
+
*/
|
|
34
|
+
const props = withDefaults(
|
|
35
|
+
defineProps<
|
|
36
|
+
DateRangeInput & {
|
|
37
|
+
actions?: DateRangeAction[]
|
|
38
|
+
borderless?: boolean
|
|
39
|
+
maxRange?: number
|
|
40
|
+
maxValue?: Date | null
|
|
41
|
+
minValue?: Date
|
|
42
|
+
}
|
|
43
|
+
>(),
|
|
44
|
+
{
|
|
45
|
+
actions: () => [],
|
|
46
|
+
borderless: false,
|
|
47
|
+
maxRange: undefined,
|
|
48
|
+
maxValue: undefined,
|
|
49
|
+
minValue: undefined,
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
const modelState = defineModel<DateRangeInput["modelValue"]>({
|
|
54
|
+
...defaultModelOpts,
|
|
55
|
+
default: { maxDate: 0, minDate: 0 },
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const selectedRange = computed<RekaDateRange>({
|
|
59
|
+
get: () => {
|
|
60
|
+
if (!modelState.value) {
|
|
61
|
+
return {
|
|
62
|
+
start: undefined,
|
|
63
|
+
end: undefined,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
start: unixToCalendarDate(modelState.value.minDate),
|
|
69
|
+
end: unixToCalendarDate(modelState.value.maxDate),
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
set: (val) => {
|
|
73
|
+
setModelState({
|
|
74
|
+
minDate: val?.start ? calendarDateToUnix(val.start) : 0,
|
|
75
|
+
maxDate: val?.end
|
|
76
|
+
? Math.floor(
|
|
77
|
+
val.end.toDate(getLocalTimeZone()).setHours(23, 59, 59, 999) / 1000
|
|
78
|
+
)
|
|
79
|
+
: 0,
|
|
80
|
+
})
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// NOTE(spk): guard against Reka write-back loop when modelState is set
|
|
85
|
+
// to zero-values as it will trigger multiple update:model-value events.
|
|
86
|
+
const setModelState = (next: NonNullable<DateRangeInput["modelValue"]>) => {
|
|
87
|
+
if (
|
|
88
|
+
modelState.value?.minDate === next.minDate &&
|
|
89
|
+
modelState.value?.maxDate === next.maxDate
|
|
90
|
+
) {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
modelState.value = next
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const unixToCalendarDate = (
|
|
98
|
+
unixSeconds?: number | null
|
|
99
|
+
): CalendarDate | undefined => {
|
|
100
|
+
if (!unixSeconds) {
|
|
101
|
+
return undefined
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const localDate = new Date(unixSeconds * 1000)
|
|
105
|
+
return new CalendarDate(
|
|
106
|
+
localDate.getFullYear(),
|
|
107
|
+
localDate.getMonth() + 1,
|
|
108
|
+
localDate.getDate()
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const bounds = computed(() => {
|
|
113
|
+
return {
|
|
114
|
+
maxValue: props.maxValue
|
|
115
|
+
? new CalendarDate(
|
|
116
|
+
props.maxValue.getFullYear(),
|
|
117
|
+
props.maxValue.getMonth() + 1,
|
|
118
|
+
props.maxValue.getDate()
|
|
119
|
+
)
|
|
120
|
+
: undefined,
|
|
121
|
+
minValue: props.minValue
|
|
122
|
+
? new CalendarDate(
|
|
123
|
+
props.minValue.getFullYear(),
|
|
124
|
+
props.minValue.getMonth() + 1,
|
|
125
|
+
props.minValue.getDate()
|
|
126
|
+
)
|
|
127
|
+
: undefined,
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const quickActions = computed(() => {
|
|
132
|
+
return props.actions.map((action) => {
|
|
133
|
+
return {
|
|
134
|
+
label: action.label,
|
|
135
|
+
action: () => {
|
|
136
|
+
setModelState(action.action(props))
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
</script>
|
|
142
|
+
|
|
143
|
+
<template>
|
|
144
|
+
<div class="w-full flex items-start justify-start">
|
|
145
|
+
<div
|
|
146
|
+
:class="[
|
|
147
|
+
'bg-white overflow-hidden flex flex-col-reverse sm:flex-row',
|
|
148
|
+
!borderless && 'border border-neutral-100 shadow rounded-xy-lg',
|
|
149
|
+
]"
|
|
150
|
+
>
|
|
151
|
+
<!--Quick Actions-->
|
|
152
|
+
<div
|
|
153
|
+
v-if="quickActions.length > 0"
|
|
154
|
+
class="bg-neutral-50 p-4 grid grid-cols-2 sm:grid-cols-1 lg:max-w-[160px]"
|
|
155
|
+
>
|
|
156
|
+
<button
|
|
157
|
+
v-for="option in quickActions"
|
|
158
|
+
:key="option.label"
|
|
159
|
+
class="flex w-full rounded-md bg-transparent hover:bg-gray-100 transition px-3 py-2 text-xs font-medium"
|
|
160
|
+
@click="option.action"
|
|
161
|
+
>
|
|
162
|
+
{{ option.label }}
|
|
163
|
+
</button>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div>
|
|
167
|
+
<!-- Calendar Input-->
|
|
168
|
+
<RangeCalendarRoot
|
|
169
|
+
v-slot="{ weekDays, grid }"
|
|
170
|
+
v-model="selectedRange"
|
|
171
|
+
class="flex space-y-4 flex-col lg:flex-row lg:space-y-0 p-4"
|
|
172
|
+
:number-of-months="1"
|
|
173
|
+
locale="en-US"
|
|
174
|
+
:maximum-days="maxRange"
|
|
175
|
+
:max-value="bounds.maxValue"
|
|
176
|
+
:min-value="bounds.minValue"
|
|
177
|
+
>
|
|
178
|
+
<div
|
|
179
|
+
v-for="(month, index) in grid"
|
|
180
|
+
:key="month.value.toString()"
|
|
181
|
+
:class="{ 'mr-4': index === 0 }"
|
|
182
|
+
>
|
|
183
|
+
<div v-if="index === 0" class="flex items-center">
|
|
184
|
+
<RangeCalendarPrev class="xy-btn-neutral-sm xy-range-cal-nav">
|
|
185
|
+
<ChevronLeftIcon class="w-4 h-4" />
|
|
186
|
+
</RangeCalendarPrev>
|
|
187
|
+
|
|
188
|
+
<!--Left Month Date Display-->
|
|
189
|
+
<span
|
|
190
|
+
class="text-sm font-semibold text-xy-black flex-1 text-center"
|
|
191
|
+
>{{
|
|
192
|
+
formatter.custom(month.value.toDate(getLocalTimeZone()), {
|
|
193
|
+
month: "long",
|
|
194
|
+
year: "numeric",
|
|
195
|
+
})
|
|
196
|
+
}}</span
|
|
197
|
+
>
|
|
198
|
+
|
|
199
|
+
<RangeCalendarNext class="xy-btn-neutral-sm xy-range-cal-nav">
|
|
200
|
+
<ChevronRightIcon class="w-4 h-4" />
|
|
201
|
+
</RangeCalendarNext>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div class="pt-4">
|
|
205
|
+
<RangeCalendarGrid class="w-full select-none space-y-1">
|
|
206
|
+
<RangeCalendarGridHead>
|
|
207
|
+
<RangeCalendarGridRow class="mb-1 grid w-full grid-cols-7">
|
|
208
|
+
<RangeCalendarHeadCell
|
|
209
|
+
v-for="day in weekDays"
|
|
210
|
+
:key="day"
|
|
211
|
+
class="text-xs text-xy-blue-600 font-bold"
|
|
212
|
+
>
|
|
213
|
+
{{ day }}
|
|
214
|
+
</RangeCalendarHeadCell>
|
|
215
|
+
</RangeCalendarGridRow>
|
|
216
|
+
</RangeCalendarGridHead>
|
|
217
|
+
|
|
218
|
+
<RangeCalendarGridBody class="grid">
|
|
219
|
+
<RangeCalendarGridRow
|
|
220
|
+
v-for="(weekDates, rowIndex) in month.rows"
|
|
221
|
+
:key="`weekDate-${rowIndex}`"
|
|
222
|
+
class="grid grid-cols-7"
|
|
223
|
+
>
|
|
224
|
+
<RangeCalendarCell
|
|
225
|
+
v-for="weekDate in weekDates"
|
|
226
|
+
:key="weekDate.toString()"
|
|
227
|
+
:date="weekDate"
|
|
228
|
+
class="xy-range-calendar-cell"
|
|
229
|
+
>
|
|
230
|
+
<RangeCalendarCellTrigger
|
|
231
|
+
:day="weekDate"
|
|
232
|
+
:month="month.value"
|
|
233
|
+
class="xy-range-cal-trigger"
|
|
234
|
+
/>
|
|
235
|
+
</RangeCalendarCell>
|
|
236
|
+
</RangeCalendarGridRow>
|
|
237
|
+
</RangeCalendarGridBody>
|
|
238
|
+
</RangeCalendarGrid>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
</RangeCalendarRoot>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
</template>
|