// interval composables import { computed, Ref, PropType } from 'vue' import { addToDate, createCalendarLocaleFormatterUTC, createIntervalList, createNativeLocaleFormatterUTC, copyTimestamp, getDateTime, getDayTimeIdentifier, getStartOfWeek, getEndOfWeek, parsed, parseTime, // updateMinutes, updateRelative, validateNumber, type Timestamp, } from '@timestamp-js/core' import { animVerticalScrollTo, animHorizontalScrollTo } from '../utils/scroll' import { toCalendarTimestamp, type CalendarScopeData } from '../utils/calendarSystem' import { type CommonProps } from './useCommon' import { type ColumnProps } from './useColumn' import { type CellWidthProps } from './useCellWidth' import { type TimesProps } from './useTimes' import { type MaxDaysProps } from './useMaxDays' import { type NavigationProps } from './useKeyboard' import useCalendarDays, { type CalendarDaysProps, type Scope, type ScopeForSlot, } from './useCalendarDays' export type { Scope, ScopeForSlot } from './useCalendarDays' export interface Resource { [key: string]: any } export interface ScopeForSlotX extends CalendarScopeData { /** Timestamp represented by the slot. */ timestamp: Timestamp /** Helper that returns the horizontal start position for a time. */ timeStartPosX: (_time: string, _clamp?: boolean) => number | false /** Helper that returns the horizontal width for a duration. */ timeDurationWidth: (_minutes: number) => number /** Zero-based rendered interval index. */ index?: number /** Whether the timestamp is outside the active month. */ outside?: boolean /** Whether the slot content is disabled. */ disabled?: boolean } export interface IntervalProps extends CommonProps, ColumnProps, CellWidthProps, MaxDaysProps, TimesProps, NavigationProps { view: 'day' | 'week' | 'month' | 'month-interval' shortIntervalLabel?: boolean intervalHeight: number | string intervalMinutes: number | string intervalStart: number | string intervalCount: number | string intervalStyle?: (_scope: Scope) => any intervalClass?: (_scope: Scope) => string weekdayStyle?: (_scope: Scope) => any weekdayClass?: (_scope: Scope) => string showIntervalLabel?: (_timestamp: Timestamp) => any hour24Format?: boolean timeClicksClamped?: boolean dateHeader: 'stacked' | 'inline' | 'inverted' } export const useIntervalProps = { /** * Calendar interval view mode. * * @category display */ view: { type: String as PropType, validator: (v: string) => ['day', 'week', 'month', 'month-interval'].includes(v), default: 'day', }, /** * Uses shortened interval labels where possible. * * @category display */ shortIntervalLabel: Boolean, /** * Height in pixels or CSS units for each time interval. * * @category layout */ intervalHeight: { type: [Number, String] as PropType, default: 40, validator: validateNumber, }, /** * Number of minutes represented by each interval. * * @category layout */ intervalMinutes: { type: [Number, String] as PropType, default: 60, validator: validateNumber, }, /** * Starting interval hour. * * @category layout */ intervalStart: { type: [Number, String] as PropType, default: 0, validator: validateNumber, }, /** * Number of intervals rendered in the day. * * @category layout */ intervalCount: { type: [Number, String] as PropType, default: 24, validator: validateNumber, }, /** * Function that returns inline styles for interval cells. * * @category style */ intervalStyle: { type: Function as PropType, default: null, }, /** * Function that returns CSS classes for interval cells. * * @category style */ intervalClass: { type: Function as PropType, default: null, }, /** * Function that returns inline styles for weekday header cells. * * @category style */ weekdayStyle: { type: Function as PropType, default: null, }, /** * Function that returns CSS classes for weekday header cells. * * @category style */ weekdayClass: { type: Function as PropType, default: null, }, /** * Function that controls whether an interval label is shown. * * @category display */ showIntervalLabel: { type: Function as PropType, default: null, }, /** * Uses 24-hour time labels. * * @category display */ hour24Format: Boolean, /** * Clamps time click calculations to interval boundaries. * * @category behavior */ timeClicksClamped: Boolean, /** * Header layout used for date labels. * * @category display */ dateHeader: { type: String as PropType, default: 'stacked', validator: (v: string) => ['stacked', 'inline', 'inverted'].includes(v), }, } as const export interface SchedulerProps { view: 'day' | 'week' | 'month' | 'month-interval' modelResources?: Resource[] resourceKey: string resourceLabel: string resourceHeight: number | string resourceMinHeight: number | string resourceStyle?: (_scope: Scope) => any resourceClass?: (_scope: Scope) => string weekdayStyle?: (_scope: Scope) => any weekdayClass?: (_scope: Scope) => string dayStyle?: (_scope: Scope) => any dayClass?: (_scope: Scope) => string dateHeader: 'stacked' | 'inline' | 'inverted' } export const useSchedulerProps = { /** * Scheduler view mode. * * @category display */ view: { type: String as PropType, validator: (v: string) => ['day', 'week', 'month', 'month-interval'].includes(v), default: 'day', }, /** * Resources rendered by the scheduler. * * @category model */ modelResources: { type: Array as PropType, }, /** * Resource field used as the unique key. * * @category model */ resourceKey: { type: String as PropType, default: 'id', }, /** * Resource field used as the display label. * * @category model */ resourceLabel: { type: String as PropType, default: 'label', }, /** * Height in pixels or CSS units for each resource row. * * @category layout */ resourceHeight: { type: [Number, String] as PropType, default: 0, validator: validateNumber, }, /** * Minimum height in pixels or CSS units for each resource row. * * @category layout */ resourceMinHeight: { type: [Number, String] as PropType, default: 70, validator: validateNumber, }, /** * Function that returns inline styles for resource rows. * * @category style */ resourceStyle: { type: Function as PropType, default: null, }, /** * Function that returns CSS classes for resource rows. * * @category style */ resourceClass: { type: Function as PropType, default: null, }, /** * Function that returns inline styles for weekday header cells. * * @category style */ weekdayStyle: { type: Function as PropType, default: null, }, /** * Function that returns CSS classes for weekday header cells. * * @category style */ weekdayClass: { type: Function as PropType, default: null, }, /** * Function that returns inline styles for day cells. * * @category style */ dayStyle: { type: Function as PropType, default: null, }, /** * Function that returns CSS classes for day cells. * * @category style */ dayClass: { type: Function as PropType, default: null, }, /** * Header layout used for date labels. * * @category display */ dateHeader: { type: String as PropType, default: 'stacked', validator: (v: string) => ['stacked', 'inline', 'inverted'].includes(v), }, } as const export interface AgendaProps { view: 'day' | 'week' | 'month' | 'month-interval' leftColumnOptions?: any[] // Consider replacing `any[]` with a more specific type. rightColumnOptions?: any[] columnOptionsId?: string columnOptionsLabel?: string weekdayStyle?: (_scope: Scope) => any weekdayClass?: (_scope: Scope) => string dayStyle?: (_scope: Scope) => any dayClass?: (_scope: Scope) => string dateHeader: 'stacked' | 'inline' | 'inverted' dayHeight: number | string dayMinHeight: number | string } export const useAgendaProps = { /** * Agenda view mode. * * @category display */ view: { type: String as PropType, validator: (v: string) => ['day', 'week', 'month', 'month-interval'].includes(v), default: 'day', }, /** * Column definitions rendered before the day columns. * * @category layout */ leftColumnOptions: { type: Array as PropType, }, /** * Column definitions rendered after the day columns. * * @category layout */ rightColumnOptions: { type: Array as PropType, }, /** * Field name used as each agenda column id. * * @category layout */ columnOptionsId: { type: String as PropType, }, /** * Field name used as each agenda column label. * * @category layout */ columnOptionsLabel: { type: String as PropType, }, /** * Function that returns inline styles for weekday header cells. * * @category style */ weekdayStyle: { type: Function as PropType, default: null, }, /** * Function that returns CSS classes for weekday header cells. * * @category style */ weekdayClass: { type: Function as PropType, default: null, }, /** * Function that returns inline styles for day cells. * * @category style */ dayStyle: { type: Function as PropType, default: null, }, /** * Function that returns CSS classes for day cells. * * @category style */ dayClass: { type: Function as PropType, default: null, }, /** * Header layout used for date labels. * * @category display */ dateHeader: { type: String as PropType, default: 'stacked', validator: (v: string) => ['stacked', 'inline', 'inverted'].includes(v), }, /** * Height in pixels or CSS units for each agenda day row. * * @category layout */ dayHeight: { type: [Number, String] as PropType, default: 0, validator: validateNumber, }, /** * Minimum height in pixels or CSS units for each agenda day row. * * @category layout */ dayMinHeight: { type: [Number, String] as PropType, default: 40, validator: validateNumber, }, } as const export interface ResourceProps extends IntervalProps { modelResources?: Resource[] resourceKey: string resourceLabel: string resourceHeight: number | string resourceMinHeight: number | string resourceStyle?: (_scope: any) => any resourceClass?: (_scope: any) => string cellWidth: number | string intervalHeaderHeight: number | string noSticky?: boolean } export const useResourceProps = { /** * Resources rendered by the resource view. * * @category model */ modelResources: { type: Array as PropType, }, /** * Resource field used as the unique key. * * @category model */ resourceKey: { type: String as PropType, default: 'id', }, /** * Resource field used as the display label. * * @category model */ resourceLabel: { type: String as PropType, default: 'label', }, /** * Height in pixels or CSS units for each resource row. * * @category layout */ resourceHeight: { type: [Number, String] as PropType, default: 0, validator: validateNumber, }, /** * Minimum height in pixels or CSS units for each resource row. * * @category layout */ resourceMinHeight: { type: [Number, String] as PropType, default: 70, validator: validateNumber, }, /** * Function that returns inline styles for resource rows. * * @category style */ resourceStyle: { type: Function as PropType, default: null, }, /** * Function that returns CSS classes for resource rows. * * @category style */ resourceClass: { type: Function as PropType, default: null, }, /** * Width in pixels or CSS units for each interval cell. * * @category layout */ cellWidth: { type: [Number, String] as PropType, default: 100, }, /** * Height in pixels or CSS units for the interval header. * * @category layout */ intervalHeaderHeight: { type: [Number, String] as PropType, default: 20, validator: validateNumber, }, /** * Disables sticky resource headers and columns. * * @category behavior */ noSticky: Boolean as PropType, } as const export interface UseIntervalReturn { parsedIntervalStart: Ref parsedIntervalMinutes: Ref parsedIntervalCount: Ref parsedIntervalHeight: Ref parsedCellWidth: Ref parsedStartMinute: Ref bodyHeight: Ref bodyWidth: Ref parsedWeekStart: Ref parsedWeekEnd: Ref days: Ref intervals: Ref intervalFormatter: Ref<(_tms: Timestamp, _short: boolean) => string> ariaDateTimeFormatter: Ref> arrayHasDateTime: (_arr: string[], _timestamp: Timestamp) => boolean checkIntervals: ( _arr: string[], _timestamp: Timestamp, ) => { firstDay: boolean; betweenDays: boolean; lastDay: boolean } getIntervalClasses: ( _interval: Timestamp, _selectedDays?: string[], _startEndDays?: string[], ) => Record getResourceClasses: ( _interval: Timestamp, _selectedDays: string[], _startEndDays: string[], ) => string[] showIntervalLabelDefault: (_interval: Timestamp) => boolean showResourceLabelDefault: (_resource: any) => void styleDefault: ({ scope }: { scope: any }) => {} getTimestampAtEventInterval: ( _e: MouseEvent & TouchEvent, _day: Timestamp, _clamp?: boolean, _now?: Timestamp, ) => Timestamp getTimestampAtEvent: ( _e: MouseEvent & TouchEvent, _day: Timestamp, _clamp?: boolean, _now?: Timestamp, ) => Timestamp getTimestampAtEventX: ( _e: MouseEvent & TouchEvent, _day: Timestamp, _clamp?: boolean, _now?: Timestamp, ) => Timestamp getScopeForSlot: (_day: Timestamp, _columnIndex: number) => ScopeForSlot getScopeForSlotX: (_day: Timestamp, _columnIndex: number) => ScopeForSlotX scrollToTime: (_time: string, _duration?: number) => boolean scrollToTimeX: (_time: string, _duration?: number) => boolean timeDurationHeight: (_minutes: number) => number timeDurationWidth: (_minutes: number) => number heightToMinutes: (_height: number) => number widthToMinutes: (_width: number) => number timeStartPos: (_time: string, _clamp?: boolean) => number | false timeStartPosX: (_time: string, _clamp?: boolean) => number | false } interface UseIntervalProps extends CalendarDaysProps { intervalHeight?: number | string intervalMinutes?: number | string intervalStart?: number | string intervalCount?: number | string hour24Format?: boolean } export default function useInterval( props: UseIntervalProps, { times, scrollArea, parsedStart, parsedEnd, activeDate, maxDays, size, headerColumnRef, }: { times: { now: Timestamp; today: Timestamp } scrollArea: Ref parsedStart: Ref parsedEnd: Ref activeDate?: Ref maxDays: Ref size: { width: number; height: number } headerColumnRef: Ref }, ): UseIntervalReturn { const { days, parsedCellWidth, styleDefault, getScopeForSlot: getCalendarDayScope, } = useCalendarDays(props, { times, parsedStart, parsedEnd, activeDate, maxDays, size, headerColumnRef, }) const parsedIntervalStart = computed(() => parseInt(String(props.intervalStart ?? 0), 10)) const parsedIntervalMinutes = computed(() => parseInt(String(props.intervalMinutes ?? 60), 10)) const parsedIntervalCount = computed(() => parseInt(String(props.intervalCount ?? 24), 10)) const parsedIntervalHeight = computed(() => parseFloat(String(props.intervalHeight ?? 40))) const parsedStartMinute = computed(() => parsedIntervalStart.value * parsedIntervalMinutes.value) const bodyHeight = computed(() => parsedIntervalCount.value * parsedIntervalHeight.value) const bodyWidth = computed(() => parsedIntervalCount.value * parsedCellWidth.value) const parsedWeekStart = computed(() => startOfWeek(parsedStart.value)) const parsedWeekEnd = computed(() => endOfWeek(parsedEnd.value)) /** * Returns an interval list for each day */ const intervals = computed(() => { return days.value.map((day) => createIntervalList( day, parsedIntervalStart.value, parsedIntervalMinutes.value, parsedIntervalCount.value, times.now, ), ) }) function startOfWeek(timestamp: Timestamp): Timestamp { return getStartOfWeek( timestamp, props.weekdays, toCalendarTimestamp(times.today, props.calendarSystem), props.calendarSystem, ) } function endOfWeek(timestamp: Timestamp): Timestamp { return getEndOfWeek( timestamp, props.weekdays, toCalendarTimestamp(times.today, props.calendarSystem), props.calendarSystem, ) } /** * Returns true if Timestamp is within passed Array of Timestamps * @param {Array.} arr * @param {Timestamp} timestamp */ function arrayHasDateTime(arr: string[], timestamp: Timestamp): boolean { return arr && arr.length > 0 && arr.includes(getDateTime(timestamp)) } /** * Takes an array of 2 Timestamps and validates the passed Timestamp (second param) * @param {Array.} arr * @param {Timestamp} timestamp * @returns {Object.<{firstDay: Boolean, betweenDays: Boolean, lastDay: Boolean}>} */ function checkIntervals( arr: string[], timestamp: Timestamp, ): { firstDay: boolean; betweenDays: boolean; lastDay: boolean } { const days = { firstDay: false, betweenDays: false, lastDay: false, } // array must have two dates ('YYYY-MM-DD HH:MM') in it if (arr && arr.length === 2) { const current = getDayTimeIdentifier(timestamp) const first = getDayTimeIdentifier(parsed(arr[0]!) as Timestamp) const last = getDayTimeIdentifier(parsed(arr[1]!) as Timestamp) days.firstDay = first === current days.lastDay = last === current days.betweenDays = first < current && last > current } return days } function getIntervalClasses( interval: Timestamp, selectedDays: string[] = [], startEndDays: string[] = [], ): Record { const isSelected = arrayHasDateTime(selectedDays, interval) const { firstDay, lastDay, betweenDays } = checkIntervals(startEndDays, interval) return { 'q-selected': isSelected, 'q-range-first': firstDay === true, 'q-range': betweenDays === true, 'q-range-last': lastDay === true, 'q-disabled-interval behavior': interval.disabled === true, } } function getResourceClasses( _interval: Timestamp, _selectedDays: string[], _startEndDays: string[], ): string[] { return [] } /** * Returns a function that uses the locale property * The function takes a timestamp and a boolean (to indicate short format) * and returns a formatted hour value from the browser */ const intervalFormatter = computed(() => { const longOptions = { timeZone: 'UTC', hour12: !props.hour24Format, hour: '2-digit', minute: '2-digit', } as const const shortOptions = { timeZone: 'UTC', hour12: !props.hour24Format, hour: 'numeric', minute: '2-digit', } as const const shortHourOptions = { timeZone: 'UTC', hour12: !props.hour24Format, hour: 'numeric', } as const return createNativeLocaleFormatterUTC(props.locale, (tms, short) => short ? (tms.minute === 0 ? shortHourOptions : shortOptions) : longOptions, ) }) /** * Returns a function that uses the locale property * The function takes a timestamp and a boolean (to indicate short format) * and returns a fully formatted timestamp string from the browser * that can be read with screen readers. * Note: This value also contains the time. */ const ariaDateTimeFormatter = computed(() => { const longOptions = { timeZone: 'UTC', dateStyle: 'full', timeStyle: 'short' } as const return createCalendarLocaleFormatterUTC(props.calendarSystem, props.locale, () => longOptions) }) /** * Determines whether the interval label should be displayed for the given timestamp. * The label is displayed if the timestamp is not the first interval and the minute is 0. * @param interval - The timestamp to check. * @returns `true` if the interval label should be displayed, `false` otherwise. */ function showIntervalLabelDefault(interval: Timestamp): boolean { const first = intervals.value[0]![0] const isFirst = first!.hour === interval.hour && first!.minute === interval.minute return !isFirst && interval.minute === 0 } function showResourceLabelDefault(_resource: any): void { // } /** * Returns a Timestamp based on mouse click position on the calendar * Also handles touch events * This function is used for vertical layout * @param {MouseEvent} e Browser MouseEvent * @param {Timestamp} day Timestamp associated with event * @param {Boolean} clamp Whether to clamp values to nearest interval * @param {Timestamp*} now Optional Timestamp for now date/time */ function getTimestampAtEventInterval( e: MouseEvent & TouchEvent, day: Timestamp, clamp = false, now?: Timestamp, ): Timestamp { let timestamp = copyTimestamp(day) if (!e.currentTarget) { return timestamp } const bounds = (e.currentTarget as HTMLElement).getBoundingClientRect() const touchEvent = e const mouseEvent = e const touches = touchEvent.changedTouches || touchEvent.touches const clientY = touches && touches[0] ? touches[0].clientY : mouseEvent.clientY const addIntervals = (clientY - bounds.top) / parsedIntervalHeight.value const addMinutes = Math.floor( (clamp ? Math.floor(addIntervals) : addIntervals) * parsedIntervalMinutes.value, ) if (addMinutes !== 0) { timestamp = addToDate(timestamp, { minute: addMinutes }, props.calendarSystem) } if (now) { timestamp = updateRelative(timestamp, now, true, props.calendarSystem) } return timestamp } /** * Returns a Timestamp based on mouse click position on the calendar * Also handles touch events * This function is used for vertical layout * @param {MouseEvent} e Browser MouseEvent * @param {Timestamp} day Timestamp associated with event * @param {Boolean} clamp Whether to clamp values to nearest interval * @param {Timestamp*} now Optional Timestamp for now date/time */ function getTimestampAtEvent( e: MouseEvent & TouchEvent, day: Timestamp, clamp = false, now?: Timestamp, ): Timestamp { let timestamp = copyTimestamp(day) const bounds = (e.currentTarget as HTMLElement).getBoundingClientRect() const touchEvent = e const mouseEvent = e const touches = touchEvent.changedTouches || touchEvent.touches const clientY = touches && touches[0] ? touches[0].clientY : mouseEvent.clientY const addIntervals = (clientY - bounds.top) / parsedIntervalHeight.value const addMinutes = Math.floor( (clamp ? Math.floor(addIntervals) : addIntervals) * parsedIntervalMinutes.value, ) if (addMinutes !== 0) { timestamp = addToDate(timestamp, { minute: addMinutes }, props.calendarSystem) } if (now) { timestamp = updateRelative(timestamp, now, true, props.calendarSystem) } return timestamp } /** * Returns a Timestamp based on mouse click position on the calendar * Also handles touch events * This function is used for horizontal layout * @param {MouseEvent} e Browser MouseEvent * @param {Timestamp} day Timestamp associated with event * @param {Boolean} clamp Whether to clamp values to nearest interval * @param {Timestamp*} now Optional Timestamp for now date/time */ function getTimestampAtEventX( e: MouseEvent & TouchEvent, day: Timestamp, clamp = false, now?: Timestamp, ): Timestamp { let timestamp = copyTimestamp(day) if (!e.currentTarget) { return timestamp } const bounds = (e.currentTarget as HTMLElement).getBoundingClientRect() const touchEvent = e const mouseEvent = e const touches = touchEvent.changedTouches || touchEvent.touches const clientX = touches && touches[0] ? touches[0].clientX : mouseEvent.clientX const addIntervals = (clientX - bounds.left) / parsedCellWidth.value const addMinutes = Math.floor( (clamp ? Math.floor(addIntervals) : addIntervals) * parsedIntervalMinutes.value, ) if (addMinutes !== 0) { timestamp = addToDate(timestamp, { minute: addMinutes }, props.calendarSystem) } if (now) { timestamp = updateRelative(timestamp, now, true, props.calendarSystem) } return timestamp } /** * Returns the scope for the associated Timestamp * This function is used for vertical layout * @param {Timestamp} timestamp * @param {Number} columnIndex */ function getScopeForSlot(timestamp: Timestamp, columnIndex?: number): ScopeForSlot { const dayScope = getCalendarDayScope(timestamp, columnIndex) const scope: ScopeForSlot = { ...dayScope, timestamp, timeStartPos, timeDurationHeight, } if (columnIndex !== undefined) { scope.columnIndex = columnIndex } return scope } /** * Returns the scope for the associated Timestamp * This function is used for horizontal layout * @param {Timestamp} timestamp * @param {Number*} index */ function getScopeForSlotX(timestamp: Timestamp, index: number): ScopeForSlotX { const dayScope = getCalendarDayScope(timestamp, index) const scope: ScopeForSlotX = { timestamp: copyTimestamp(timestamp), calendarTimestamp: dayScope.calendarTimestamp, calendarIdentity: dayScope.calendarIdentity, calendarSystem: dayScope.calendarSystem, timeStartPosX, timeDurationWidth, outside: dayScope.outside, disabled: dayScope.disabled, } if (index !== undefined) { scope.index = index } return scope } /** * Forces the browser to scroll to the specified time * This function is used for vertical layout * @param {String} time in format HH:MM * @param {Number} duration in milliseconds * @returns {boolean} Whether the scroll operation was successful */ function scrollToTime(time: string, duration = 0): boolean { const y = timeStartPos(time) if (y === false || !scrollArea.value) { return false } animVerticalScrollTo(scrollArea.value, y, duration) return true } /** * Forces the browser to scroll to the specified time horizontally. * This function is used for horizontal layout. * @param {String} time - The time to scroll to, in the format HH:MM. * @param {Number} [duration=0] - The duration of the scroll behavior in milliseconds. * @returns {boolean} Whether the scroll operation was successful. */ function scrollToTimeX(time: string, duration = 0): boolean { const x = timeStartPosX(time) if (x === false || !scrollArea.value) { return false } animHorizontalScrollTo(scrollArea.value, x, duration) return true } /** * Calculates the height of a time duration in the interval display. * @param {number} minutes - The number of minutes to calculate the height for. * @returns {number} The height of the time duration in pixels. */ function timeDurationHeight(minutes: number): number { return (minutes / parsedIntervalMinutes.value) * parsedIntervalHeight.value } /** * Calculates the width of a time duration in the interval display. * @param {number} minutes - The number of minutes to calculate the width for. * @returns {number} The width of the time duration in pixels. */ function timeDurationWidth(minutes: number): number { return (minutes / parsedIntervalMinutes.value) * parsedCellWidth.value } /** * Calculates the number of minutes represented by a given height in the interval display. * @param {number} height - The height in pixels to calculate the minutes for. * @returns {number} The number of minutes represented by the given height. */ function heightToMinutes(height: number): number { return (height * parsedIntervalMinutes.value) / parsedIntervalHeight.value } /** * Calculates the number of minutes represented by a given width in the interval display. * @param {number} width - The width in pixels to calculate the minutes for. * @returns {number} The number of minutes represented by the given width. */ function widthToMinutes(width: number): number { return (width * parsedIntervalMinutes.value) / parsedCellWidth.value } /** * Calculates the starting position (y-coordinate) of a time value in the interval display. * @param {string} time - The time value to calculate the starting position for. * @param {boolean} [clamp=true] - Whether to clamp the calculated position to the bounds of the interval display. * @returns {number|false} The starting position (y-coordinate) of the time value, or `false` if the time value is invalid. */ function timeStartPos(time: string, clamp = true): number | false { const minutes = parseTime(time) if (minutes === false) return false const min = parsedStartMinute.value const gap = parsedIntervalCount.value * parsedIntervalMinutes.value const delta = (minutes - min) / gap let y = delta * bodyHeight.value if (clamp) { if (y < 0) { y = 0 } if (y > bodyHeight.value) { y = bodyHeight.value } } return y } /** * Calculates the starting position (x-coordinate) of a time value in the interval display. * @param {string} time - The time value to calculate the starting position for. * @param {boolean} [clamp=true] - Whether to clamp the calculated position to the bounds of the interval display. * @returns {number|false} The starting position (x-coordinate) of the time value, or `false` if the time value is invalid. */ function timeStartPosX(time: string, clamp = true): number | false { const minutes = parseTime(time) if (minutes === false) return false const min = parsedStartMinute.value const gap = parsedIntervalCount.value * parsedIntervalMinutes.value const delta = (minutes - min) / gap let x = delta * bodyWidth.value if (clamp) { if (x < 0) { x = 0 } if (x > bodyWidth.value) { x = bodyWidth.value } } return x } return { parsedIntervalStart, parsedIntervalMinutes, parsedIntervalCount, parsedIntervalHeight, parsedCellWidth, parsedStartMinute, bodyHeight, bodyWidth, parsedWeekStart, parsedWeekEnd, days, intervals, intervalFormatter, ariaDateTimeFormatter, arrayHasDateTime, checkIntervals, getIntervalClasses, getResourceClasses, showIntervalLabelDefault, showResourceLabelDefault, styleDefault, getTimestampAtEventInterval, getTimestampAtEvent, getTimestampAtEventX, getScopeForSlot, getScopeForSlotX, scrollToTime, scrollToTimeX, timeDurationHeight, timeDurationWidth, heightToMinutes, widthToMinutes, timeStartPos, timeStartPosX, } }