reka-ui
Version:
Vue port for Radix UI Primitives.
221 lines (187 loc) • 6.64 kB
text/typescript
import type { DateFields, DateValue } from '@internationalized/date'
import type { Ref } from 'vue'
import type { Formatter } from '@/shared'
import type { DateSegmentPart, Granularity, HourCycle, SegmentContentObj, SegmentPart, SegmentValueObj, TimeSegmentPart } from '@/shared/date'
import { isZonedDateTime, toDate } from '@/date'
import { DATE_SEGMENT_PARTS, EDITABLE_SEGMENT_PARTS, getOptsByGranularity, getPlaceholder, isDateSegmentPart, isSegmentPart, normalizeHourCycle, TIME_SEGMENT_PARTS } from '@/shared/date'
const calendarDateTimeGranularities = ['hour', 'minute', 'second']
type SyncDateSegmentValuesProps = {
value: DateValue
formatter: Formatter
}
type SyncTimeSegmentValuesProps = {
value: DateValue
formatter: Formatter
}
export function syncTimeSegmentValues(props: SyncTimeSegmentValuesProps) {
return Object.fromEntries(TIME_SEGMENT_PARTS.map((part) => {
if (part === 'dayPeriod')
return [part, props.formatter.dayPeriod(toDate(props.value))]
return [part, props.value[part as keyof DateValue]]
})) as SegmentValueObj
}
export function syncSegmentValues(props: SyncDateSegmentValuesProps) {
const { formatter } = props
const dateValues = DATE_SEGMENT_PARTS.map((part) => {
return [part, props.value[part]]
})
if ('hour' in props.value) {
const timeValues = syncTimeSegmentValues({ value: props.value, formatter })
return { ...Object.fromEntries(dateValues), ...timeValues } as SegmentValueObj
}
return Object.fromEntries(dateValues) as SegmentValueObj
}
export function initializeTimeSegmentValues(granularity: 'hour' | 'minute' | 'second'): SegmentValueObj {
return Object.fromEntries(
TIME_SEGMENT_PARTS.map((part) => {
if (part === 'dayPeriod')
return [part, 'AM']
return [part, null]
}).filter(([key]) => {
if (key === 'literal' || key === null)
return false
if (granularity === 'minute' && key === 'second')
return false
if (granularity === 'hour' && (key === 'second' || key === 'minute'))
return false
else return true
}),
)
}
export function initializeSegmentValues(granularity: Granularity): SegmentValueObj {
const initialParts = EDITABLE_SEGMENT_PARTS.map((part) => {
if (part === 'dayPeriod')
return [part, 'AM']
return [part, null]
}).filter(([key]) => {
if (key === 'literal' || key === null)
return false
if (granularity === 'minute' && key === 'second')
return false
if (granularity === 'hour' && (key === 'second' || key === 'minute'))
return false
if (granularity === 'day')
return !calendarDateTimeGranularities.includes(key) && key !== 'dayPeriod'
else return true
})
return Object.fromEntries(initialParts)
}
type SharedContentProps = {
granularity: Granularity
dateRef: DateValue
formatter: Formatter
hideTimeZone: boolean
hourCycle: HourCycle
isTimeValue?: boolean
}
type CreateContentObjProps = SharedContentProps & {
segmentValues: SegmentValueObj
locale: Ref<string>
}
type CreateContentArrProps = SharedContentProps & {
contentObj: SegmentContentObj
}
function createContentObj(props: CreateContentObjProps) {
const { segmentValues, formatter, locale } = props
function getPartContent(part: DateSegmentPart | TimeSegmentPart) {
if ('hour' in segmentValues) {
const value = segmentValues[part]
if (value !== null) {
if (part === 'day') {
return formatter.part(props.dateRef.set({
[part as keyof DateFields]: value,
/**
* Edge case for the day field:
*
* 1. If the month is filled,
* we need to ensure that the day snaps to the maximum value of that month.
* 2. If the month is not filled,
* we default to the month with the maximum number of days (here just using January, 31 days),
* so that user can input any possible day.
*/
month: segmentValues.month ?? 1,
}), part, { hourCycle: normalizeHourCycle(props.hourCycle) })
}
return formatter.part(props.dateRef.set({ [part]: value }), part, {
hourCycle: normalizeHourCycle(props.hourCycle),
})
}
else {
return getPlaceholder(part, '', locale.value)
}
}
else {
if (isDateSegmentPart(part)) {
const value = segmentValues[part]
if (value !== null) {
if (part === 'day') {
return formatter.part(props.dateRef.set({
[part]: value,
// Same logic as above for the day field
month: segmentValues.month ?? 1,
}), part)
}
return formatter.part(props.dateRef.set({ [part]: value }), part)
}
else {
return getPlaceholder(part, '', locale.value)
}
}
return ''
}
}
const content = Object.keys(segmentValues).reduce((obj, part) => {
if (!isSegmentPart(part))
return obj
if ('hour' in segmentValues && part === 'dayPeriod') {
const value = segmentValues[part]
if (value !== null)
obj[part] = value
else
obj[part] = getPlaceholder(part, 'AM', locale.value)
}
else {
obj[part] = getPartContent(part)
}
return obj
}, {} as SegmentContentObj)
return content
}
function createContentArr(props: CreateContentArrProps) {
const { granularity, formatter, contentObj, hideTimeZone, hourCycle, isTimeValue } = props
const parts = formatter.toParts(props.dateRef, getOptsByGranularity(granularity, hourCycle, isTimeValue))
const segmentContentArr = parts
.map((part) => {
const defaultParts = ['literal', 'timeZoneName', null]
if (defaultParts.includes(part.type) || !isSegmentPart(part.type)) {
return {
part: part.type,
value: part.value,
}
}
return {
part: part.type,
value: contentObj[part.type],
}
})
.filter((segment): segment is { part: SegmentPart, value: string } => {
if (segment.part === null || segment.value === null)
return false
if (segment.part === 'timeZoneName' && (!isZonedDateTime(props.dateRef) || hideTimeZone))
return false
return true
})
return segmentContentArr
}
type CreateContentProps = CreateContentObjProps
export function createContent(props: CreateContentProps) {
const contentObj = createContentObj(props)
const contentArr = createContentArr({
contentObj,
...props,
})
return {
obj: contentObj,
arr: contentArr,
}
}