@oslokommune/punkt-elements
Version:
Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo
249 lines (228 loc) • 6.51 kB
text/typescript
import { fromISOToDate, newDate } from 'shared-utils/date-utils'
import { Ref } from 'lit/directives/ref.js'
import { PktCalendar } from '@/components/calendar/calendar'
import {
validateRangeOrder,
sortDates,
filterDates,
getDatepickerInputType,
getDatepickerInputClasses,
getDatepickerButtonClasses,
getRangeLabelClasses,
processDateSelection as sharedProcessDateSelection,
handleCalendarPosition as sharedHandleCalendarPosition,
handleDatepickerKeydown,
handleDatepickerButtonKeydown,
} from 'shared-utils/datepicker-utils'
/**
* Utility functions for PktDatepicker component
*
* This module provides helper functions organized by concern.
* Framework-agnostic functions are delegated to shared-utils/datepicker-utils.
* Lit-specific functions (using Ref, ElementInternals, PktCalendar) stay here.
*/
/**
* Value parsing and validation utilities
* Delegates to shared-utils/datepicker-utils
*/
export const valueUtils = {
validateRangeOrder,
sortDates,
filterSelectableDates: filterDates,
}
/**
* Input type detection utilities
* Delegates to shared-utils/datepicker-utils
*/
export const inputTypeUtils = {
getInputType: getDatepickerInputType,
}
/**
* Form and validation utilities (Lit-specific — uses ElementInternals)
*/
export const formUtils = {
submitForm(element: HTMLElement): void {
const form = (element as any).internals?.form as HTMLFormElement
if (form) {
form.requestSubmit()
}
},
submitFormOrFallback(internals: any, fallbackAction: () => void): void {
const form = internals?.form as HTMLFormElement
if (form) {
form.requestSubmit()
} else {
fallbackAction()
}
},
validateDateInput(
input: HTMLInputElement,
internals: any,
min?: string | null,
max?: string | null,
strings?: any,
): void {
const value = input.value
if (!value) return
if (min && min > value) {
internals.setValidity(
{ rangeUnderflow: true },
strings?.forms?.messages?.rangeUnderflow || 'Value is below minimum',
input,
)
} else if (max && max < value) {
internals.setValidity(
{ rangeOverflow: true },
strings?.forms?.messages?.rangeOverflow || 'Value is above maximum',
input,
)
}
},
}
/**
* Calendar interaction utilities
* handleCalendarPosition delegates to shared-utils (with Lit ref unwrapping).
* addToSelected stays Lit-specific (uses Ref<PktCalendar>).
*/
export const calendarUtils = {
addToSelected(
event: Event | KeyboardEvent,
calendarRef: Ref<PktCalendar>,
min?: string | null,
max?: string | null,
): void {
const target = event.target as HTMLInputElement
if (!target.value) return
const minAsDate = min ? newDate(min) : null
const maxAsDate = max ? newDate(max) : null
const date = newDate(target.value.split(',')[0])
if (
date &&
!isNaN(date.getTime()) &&
(!minAsDate || date >= minAsDate) &&
(!maxAsDate || date <= maxAsDate) &&
calendarRef.value
) {
calendarRef.value.handleDateSelect(date)
}
target.value = ''
},
handleCalendarPosition(
popupRef: Ref<HTMLDivElement>,
inputRef: Ref<HTMLInputElement>,
hasCounter: boolean = false,
): void {
sharedHandleCalendarPosition(popupRef.value ?? null, inputRef.value ?? null, hasCounter)
},
}
/**
* Event handling utilities (Lit-specific — uses Lit Ref types)
*/
export const eventUtils = {
createDocumentClickListener(
inputRef: Ref<HTMLInputElement>,
inputRefTo: Ref<HTMLInputElement> | null,
btnRef: Ref<HTMLButtonElement>,
getCalendarOpen: () => boolean,
onBlur: () => void,
hideCalendar: () => void,
): (e: MouseEvent) => void {
return (e: MouseEvent) => {
if (
inputRef?.value &&
btnRef?.value &&
!inputRef.value.contains(e.target as Node) &&
!(inputRefTo?.value && inputRefTo.value.contains(e.target as Node)) &&
!btnRef.value.contains(e.target as Node) &&
!(e.target as Element).closest('.pkt-calendar-popup') &&
getCalendarOpen()
) {
onBlur()
hideCalendar()
}
}
},
createDocumentKeydownListener(
getCalendarOpen: () => boolean,
hideCalendar: () => void,
): (e: KeyboardEvent) => void {
return (e: KeyboardEvent) => {
if (e.key === 'Escape' && getCalendarOpen()) {
hideCalendar()
}
}
},
handleFocusOut(
event: FocusEvent,
element: HTMLElement,
onBlur: () => void,
hideCalendar: () => void,
): void {
if (!element.contains(event.target as Node)) {
onBlur()
hideCalendar()
}
},
}
/**
* CSS class utilities
* Delegates to shared-utils/datepicker-utils
*/
export const cssUtils = {
getInputClasses: getDatepickerInputClasses,
getButtonClasses: getDatepickerButtonClasses,
getRangeLabelClasses,
}
/**
* Date value processing utilities
* processDateSelection delegates to shared-utils.
* updateInputValues and processRangeBlur stay Lit-specific (use Lit Ref).
*/
export const dateProcessingUtils = {
processDateSelection: sharedProcessDateSelection,
updateInputValues(
inputRef: Ref<HTMLInputElement>,
inputRefTo: Ref<HTMLInputElement> | null,
values: string[],
range: boolean,
multiple: boolean,
manageValidity: (input: HTMLInputElement) => void,
): void {
if (!inputRef.value) return
if (range && inputRefTo?.value) {
inputRef.value.value = values[0] ?? ''
inputRefTo.value.value = values[1] ?? ''
manageValidity(inputRef.value)
manageValidity(inputRefTo.value)
} else if (!multiple) {
inputRef.value.value = values.length ? values[0] : ''
manageValidity(inputRef.value)
}
},
processRangeBlur(
event: Event,
values: string[],
calendarRef: Ref<PktCalendar>,
clearInputValue: () => void,
manageValidity: (input: HTMLInputElement) => void,
): void {
const target = event.target as HTMLInputElement
if (target.value) {
manageValidity(target)
const date = fromISOToDate(target.value)
if (date) {
calendarRef?.value?.handleDateSelect(date)
}
} else if (values[0]) {
clearInputValue()
}
},
}
/**
* Keyboard navigation utilities
* Delegates to shared-utils/datepicker-utils
*/
export const keyboardUtils = {
handleInputKeydown: handleDatepickerKeydown,
handleButtonKeydown: handleDatepickerButtonKeydown,
}