@oslokommune/punkt-elements 14.0.2 → 14.0.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.
Files changed (30) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/calendar-BtShW7ER.cjs +90 -0
  3. package/dist/{calendar-Bz27nuTP.js → calendar-yxjSI4wd.js} +766 -682
  4. package/dist/datepicker-D0q75U1Z.js +1463 -0
  5. package/dist/datepicker-DDV382Uu.cjs +271 -0
  6. package/dist/index.d.ts +118 -83
  7. package/dist/pkt-calendar.cjs +1 -1
  8. package/dist/pkt-calendar.js +1 -1
  9. package/dist/pkt-datepicker.cjs +1 -1
  10. package/dist/pkt-datepicker.js +2 -2
  11. package/dist/pkt-index.cjs +1 -1
  12. package/dist/pkt-index.js +3 -3
  13. package/package.json +2 -2
  14. package/src/components/calendar/calendar.ts +372 -414
  15. package/src/components/calendar/helpers/calendar-grid.ts +93 -0
  16. package/src/components/calendar/helpers/date-validation.ts +86 -0
  17. package/src/components/calendar/helpers/index.ts +49 -0
  18. package/src/components/calendar/helpers/keyboard-navigation.ts +54 -0
  19. package/src/components/calendar/helpers/selection-manager.ts +184 -0
  20. package/src/components/datepicker/datepicker-base.ts +151 -0
  21. package/src/components/datepicker/datepicker-multiple.ts +7 -114
  22. package/src/components/datepicker/datepicker-range.ts +21 -141
  23. package/src/components/datepicker/datepicker-single.ts +7 -115
  24. package/src/components/datepicker/datepicker-types.ts +56 -0
  25. package/src/components/datepicker/datepicker-utils.test.ts +730 -0
  26. package/src/components/datepicker/datepicker-utils.ts +338 -9
  27. package/src/components/datepicker/datepicker.ts +25 -1
  28. package/dist/calendar-Dz1Cnzx5.cjs +0 -115
  29. package/dist/datepicker-CnCOXI2x.cjs +0 -289
  30. package/dist/datepicker-DsqM01iU.js +0 -1355
@@ -0,0 +1,93 @@
1
+ import { newDateYMD } from 'shared-utils/date-utils'
2
+ import { getWeek } from 'date-fns'
3
+
4
+ // Constants
5
+ export const DAYS_PER_WEEK = 7
6
+ export const MONDAY_OFFSET = 6
7
+
8
+ /**
9
+ * Calendar grid calculation helpers
10
+ */
11
+
12
+ export interface ICalendarGridDimensions {
13
+ firstDayOfMonth: Date
14
+ lastDayOfMonth: Date
15
+ startingDay: number
16
+ numDays: number
17
+ numRows: number
18
+ numDaysPrevMonth: number
19
+ initialWeek: number
20
+ }
21
+
22
+ export interface IGridCellPosition {
23
+ rowIndex: number
24
+ colIndex: number
25
+ }
26
+
27
+ /**
28
+ * Calculate the dimensions and structure of the calendar grid
29
+ */
30
+ export function calculateCalendarDimensions(year: number, month: number): ICalendarGridDimensions {
31
+ const firstDayOfMonth = newDateYMD(year, month, 1)
32
+ const lastDayOfMonth = newDateYMD(year, month + 1, 0)
33
+ const startingDay = (firstDayOfMonth.getDay() + MONDAY_OFFSET) % DAYS_PER_WEEK
34
+ const numDays = lastDayOfMonth.getDate()
35
+ const numRows = Math.ceil((numDays + startingDay) / DAYS_PER_WEEK)
36
+ const lastDayOfPrevMonth = newDateYMD(year, month, 0)
37
+ const numDaysPrevMonth = lastDayOfPrevMonth.getDate()
38
+ const initialWeek = getWeek(firstDayOfMonth)
39
+
40
+ return {
41
+ firstDayOfMonth,
42
+ lastDayOfMonth,
43
+ startingDay,
44
+ numDays,
45
+ numRows,
46
+ numDaysPrevMonth,
47
+ initialWeek,
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Determine what type of cell to render at a given position
53
+ */
54
+ export function getCellType(
55
+ rowIndex: number,
56
+ colIndex: number,
57
+ dayCounter: number,
58
+ gridDimensions: ICalendarGridDimensions,
59
+ ): 'prev-month' | 'current-month' | 'next-month' {
60
+ const { startingDay, numDays } = gridDimensions
61
+
62
+ if (rowIndex === 0 && colIndex < startingDay) {
63
+ return 'prev-month'
64
+ }
65
+
66
+ if (dayCounter > numDays) {
67
+ return 'next-month'
68
+ }
69
+
70
+ return 'current-month'
71
+ }
72
+
73
+ /**
74
+ * Get the day number to display for a given cell
75
+ */
76
+ export function getDayNumber(
77
+ cellType: 'prev-month' | 'current-month' | 'next-month',
78
+ colIndex: number,
79
+ dayCounter: number,
80
+ gridDimensions: ICalendarGridDimensions,
81
+ ): number {
82
+ const { startingDay, numDaysPrevMonth, numDays } = gridDimensions
83
+
84
+ if (cellType === 'prev-month') {
85
+ return numDaysPrevMonth - (startingDay - colIndex - 1)
86
+ }
87
+
88
+ if (cellType === 'next-month') {
89
+ return dayCounter - numDays
90
+ }
91
+
92
+ return dayCounter
93
+ }
@@ -0,0 +1,86 @@
1
+ import { formatISODate, isDateSelectable, newDate, newDateYMD } from 'shared-utils/date-utils'
2
+
3
+ /**
4
+ * Date validation helpers for calendar component
5
+ */
6
+
7
+ export interface IDateConstraints {
8
+ earliest: string | null
9
+ latest: string | null
10
+ excludedates: Date[]
11
+ excludeweekdays: string[]
12
+ }
13
+
14
+ /**
15
+ * Check if a date is excluded based on constraints
16
+ */
17
+ export function isDateExcluded(date: Date, constraints: IDateConstraints): boolean {
18
+ const excludedDatesStrings = constraints.excludedates.map((d) =>
19
+ typeof d === 'string' ? d : formatISODate(d),
20
+ )
21
+
22
+ return !isDateSelectable(
23
+ date,
24
+ constraints.earliest,
25
+ constraints.latest,
26
+ excludedDatesStrings,
27
+ constraints.excludeweekdays,
28
+ )
29
+ }
30
+
31
+ /**
32
+ * Check if a day should be disabled for selection
33
+ */
34
+ export function isDayDisabled(
35
+ date: Date,
36
+ isSelected: boolean,
37
+ constraints: IDateConstraints,
38
+ options: {
39
+ multiple: boolean
40
+ maxMultiple: number
41
+ selectedCount: number
42
+ },
43
+ ): boolean {
44
+ if (isDateExcluded(date, constraints)) return true
45
+
46
+ if (
47
+ !isSelected &&
48
+ options.multiple &&
49
+ options.maxMultiple > 0 &&
50
+ options.selectedCount >= options.maxMultiple
51
+ ) {
52
+ return true
53
+ }
54
+
55
+ return false
56
+ }
57
+
58
+ /**
59
+ * Check if navigation to previous month is allowed
60
+ */
61
+ export function isPrevMonthAllowed(
62
+ year: number,
63
+ month: number,
64
+ earliest: string | null,
65
+ ): boolean {
66
+ const prevMonth = newDateYMD(year, month, 0)
67
+ if (earliest && newDate(earliest) > prevMonth) return false
68
+ return true
69
+ }
70
+
71
+ /**
72
+ * Check if navigation to next month is allowed
73
+ */
74
+ export function isNextMonthAllowed(
75
+ year: number,
76
+ month: number,
77
+ latest: string | null,
78
+ ): boolean {
79
+ const nextMonth = newDateYMD(
80
+ month === 11 ? year + 1 : year,
81
+ month === 11 ? 0 : month + 1,
82
+ 1,
83
+ )
84
+ if (latest && newDate(latest) < nextMonth) return false
85
+ return true
86
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Calendar Helper Exports
3
+ *
4
+ * This module re-exports all calendar helper functions and types
5
+ * to provide a single import point for calendar-related utilities.
6
+ */
7
+
8
+ // Date validation
9
+ export {
10
+ isDateExcluded,
11
+ isDayDisabled,
12
+ isPrevMonthAllowed,
13
+ isNextMonthAllowed,
14
+ type IDateConstraints,
15
+ } from './date-validation'
16
+
17
+ // Calendar grid calculations
18
+ export {
19
+ DAYS_PER_WEEK,
20
+ MONDAY_OFFSET,
21
+ calculateCalendarDimensions,
22
+ getCellType,
23
+ getDayNumber,
24
+ type ICalendarGridDimensions,
25
+ type IGridCellPosition,
26
+ } from './calendar-grid'
27
+
28
+ // Selection management
29
+ export {
30
+ convertSelectedToDates,
31
+ updateRangeMap,
32
+ isRangeAllowed,
33
+ addToSelection,
34
+ removeFromSelection,
35
+ toggleSelection,
36
+ handleRangeSelection,
37
+ type TDateRangeMap,
38
+ type TSelectionMode,
39
+ type ISelectionState,
40
+ type ISelectionOptions,
41
+ } from './selection-manager'
42
+
43
+ // Keyboard navigation
44
+ export {
45
+ KEY_DIRECTION_MAP,
46
+ shouldIgnoreKeyboardEvent,
47
+ findNextSelectableDate,
48
+ getKeyDirection,
49
+ } from './keyboard-navigation'
@@ -0,0 +1,54 @@
1
+ import { addDays } from 'date-fns'
2
+ import { formatISODate } from 'shared-utils/date-utils'
3
+ import { DAYS_PER_WEEK } from './calendar-grid'
4
+
5
+ /**
6
+ * Keyboard navigation helpers for calendar component
7
+ */
8
+
9
+ export const KEY_DIRECTION_MAP: Record<string, number> = {
10
+ ArrowLeft: -1,
11
+ ArrowRight: 1,
12
+ ArrowUp: -DAYS_PER_WEEK,
13
+ ArrowDown: DAYS_PER_WEEK,
14
+ }
15
+
16
+ /**
17
+ * Check if a keyboard event should be ignored
18
+ */
19
+ export function shouldIgnoreKeyboardEvent(target: HTMLElement): boolean {
20
+ const nodeName = target.nodeName
21
+ if (nodeName === 'INPUT' || nodeName === 'SELECT') return true
22
+ if (nodeName === 'BUTTON' && !target.dataset?.date) return true
23
+ return false
24
+ }
25
+
26
+ /**
27
+ * Find the next selectable date in a given direction
28
+ */
29
+ export function findNextSelectableDate(
30
+ startDate: Date,
31
+ direction: number,
32
+ querySelector: (selector: string) => Element | null,
33
+ ): Date | null {
34
+ let nextDate = addDays(startDate, direction)
35
+ if (!nextDate) return null
36
+
37
+ let el = querySelector(`button[data-date="${formatISODate(nextDate)}"]`)
38
+
39
+ // Skip disabled dates
40
+ while (el instanceof HTMLButtonElement && el.dataset.disabled) {
41
+ nextDate = addDays(nextDate, direction)
42
+ el = querySelector(`button[data-date="${formatISODate(nextDate)}"]`)
43
+ if (!el) return null
44
+ }
45
+
46
+ return nextDate
47
+ }
48
+
49
+ /**
50
+ * Get the direction for a keyboard event
51
+ */
52
+ export function getKeyDirection(key: string): number | null {
53
+ return KEY_DIRECTION_MAP[key] ?? null
54
+ }
@@ -0,0 +1,184 @@
1
+ import { formatISODate, parseISODateString } from 'shared-utils/date-utils'
2
+ import { eachDayOfInterval, getISODay } from 'date-fns'
3
+
4
+ /**
5
+ * Selection management helpers for calendar component
6
+ */
7
+
8
+ export type TSelectionMode = 'single' | 'multiple' | 'range'
9
+
10
+ export type TDateRangeMap = {
11
+ [key: string]: boolean
12
+ }
13
+
14
+ export interface ISelectionState {
15
+ selected: string[]
16
+ _selected: Date[]
17
+ inRange: TDateRangeMap
18
+ }
19
+
20
+ export interface ISelectionOptions {
21
+ multiple: boolean
22
+ maxMultiple: number
23
+ range: boolean
24
+ excludedates: Date[]
25
+ excludeweekdays: string[]
26
+ }
27
+
28
+ /**
29
+ * Convert selected string array to Date array
30
+ */
31
+ export function convertSelectedToDates(selected: string | string[]): Date[] {
32
+ if (typeof selected === 'string') {
33
+ selected = selected.split(',')
34
+ }
35
+ if (selected.length === 1 && selected[0] === '') {
36
+ return []
37
+ }
38
+ return selected.map((d: string) => parseISODateString(d))
39
+ }
40
+
41
+ /**
42
+ * Update the range map for visualizing date ranges
43
+ */
44
+ export function updateRangeMap(start: Date, end: Date): TDateRangeMap {
45
+ const days = eachDayOfInterval({ start, end })
46
+ const inRange: TDateRangeMap = {}
47
+
48
+ if (Array.isArray(days) && days.length) {
49
+ for (let i = 0; i < days.length; i++) {
50
+ const day = days[i]
51
+ const isInRange = day > start && day < end
52
+ inRange[formatISODate(day)] = isInRange
53
+ }
54
+ }
55
+
56
+ return inRange
57
+ }
58
+
59
+ /**
60
+ * Check if a range selection is allowed (no excluded dates in between)
61
+ */
62
+ export function isRangeAllowed(
63
+ date: Date,
64
+ selectedDates: Date[],
65
+ excludedates: Date[],
66
+ excludeweekdays: string[],
67
+ ): boolean {
68
+ if (selectedDates.length !== 1) return true
69
+
70
+ const days = eachDayOfInterval({
71
+ start: selectedDates[0],
72
+ end: date,
73
+ })
74
+
75
+ if (!Array.isArray(days) || !days.length) return true
76
+
77
+ for (let i = 0; i < days.length; i++) {
78
+ // Check excluded dates
79
+ for (const excludedDate of excludedates) {
80
+ if (excludedDate > selectedDates[0] && excludedDate < date) {
81
+ return false
82
+ }
83
+ }
84
+
85
+ // Check excluded weekdays
86
+ if (excludeweekdays.includes(getISODay(days[i]).toString())) {
87
+ return false
88
+ }
89
+ }
90
+
91
+ return true
92
+ }
93
+
94
+ /**
95
+ * Add a date to the selection
96
+ */
97
+ export function addToSelection(
98
+ selectedDate: Date,
99
+ currentSelected: string[],
100
+ ): string[] {
101
+ const dateISO = formatISODate(selectedDate)
102
+ if (currentSelected.includes(dateISO)) {
103
+ return currentSelected
104
+ }
105
+ return [...currentSelected, dateISO]
106
+ }
107
+
108
+ /**
109
+ * Remove a date from the selection
110
+ */
111
+ export function removeFromSelection(
112
+ selectedDate: Date,
113
+ currentSelected: string[],
114
+ ): string[] {
115
+ const dateISO = formatISODate(selectedDate)
116
+ const index = currentSelected.indexOf(dateISO)
117
+
118
+ if (index === -1) return currentSelected
119
+ if (currentSelected.length === 1) return []
120
+
121
+ const newSelected = [...currentSelected]
122
+ newSelected.splice(index, 1)
123
+ return newSelected
124
+ }
125
+
126
+ /**
127
+ * Toggle a date in the selection
128
+ */
129
+ export function toggleSelection(
130
+ selectedDate: Date,
131
+ currentSelected: string[],
132
+ maxMultiple: number,
133
+ ): string[] {
134
+ const dateISO = formatISODate(selectedDate)
135
+
136
+ if (currentSelected.includes(dateISO)) {
137
+ return removeFromSelection(selectedDate, currentSelected)
138
+ }
139
+
140
+ if (maxMultiple > 0 && currentSelected.length >= maxMultiple) {
141
+ return currentSelected
142
+ }
143
+
144
+ return addToSelection(selectedDate, currentSelected)
145
+ }
146
+
147
+ /**
148
+ * Handle range selection logic
149
+ */
150
+ export function handleRangeSelection(
151
+ selectedDate: Date,
152
+ currentSelected: string[],
153
+ options: Pick<ISelectionOptions, 'excludedates' | 'excludeweekdays'>,
154
+ ): string[] {
155
+ const dateISO = formatISODate(selectedDate)
156
+ const selectedDates = convertSelectedToDates(currentSelected)
157
+
158
+ // If clicking on already selected date
159
+ if (currentSelected.includes(dateISO)) {
160
+ // If it's the first date, clear selection
161
+ if (currentSelected.indexOf(dateISO) === 0) {
162
+ return []
163
+ }
164
+ // Otherwise remove it
165
+ return removeFromSelection(selectedDate, currentSelected)
166
+ }
167
+
168
+ // If we have more than 1 selection, start over
169
+ if (currentSelected.length > 1) {
170
+ return [dateISO]
171
+ }
172
+
173
+ // If we have 1 selection, check if range is valid
174
+ if (currentSelected.length === 1) {
175
+ if (!isRangeAllowed(selectedDate, selectedDates, options.excludedates, options.excludeweekdays)) {
176
+ return [dateISO]
177
+ }
178
+ if (selectedDates[0] > selectedDate) {
179
+ return [dateISO]
180
+ }
181
+ }
182
+
183
+ return addToSelection(selectedDate, currentSelected)
184
+ }
@@ -0,0 +1,151 @@
1
+ import { html, TemplateResult } from 'lit'
2
+ import { property } from 'lit/decorators.js'
3
+ import { Ref, createRef } from 'lit/directives/ref.js'
4
+ import { classMap } from 'lit/directives/class-map.js'
5
+ import { PktElement } from '@/base-elements/element'
6
+ import { cssUtils } from './datepicker-utils'
7
+ import { IDatepickerStrings } from './datepicker-types'
8
+ import '@/components/icon'
9
+
10
+ /**
11
+ * Abstract base class for datepicker input components
12
+ *
13
+ * Consolidates shared properties, methods, and event dispatchers
14
+ * used by all three datepicker input types (single, multiple, range).
15
+ *
16
+ * Subclasses must implement:
17
+ * - `strings` property with component-specific defaults
18
+ * - `render()` method with component-specific template
19
+ */
20
+ export abstract class PktDatepickerBase extends PktElement {
21
+ // Shared properties (9 identical across all sub-components)
22
+ @property({ type: String })
23
+ inputType: string = 'date'
24
+
25
+ @property({ type: String })
26
+ id: string = ''
27
+
28
+ @property({ type: String })
29
+ min?: string
30
+
31
+ @property({ type: String })
32
+ max?: string
33
+
34
+ @property({ type: String })
35
+ placeholder?: string
36
+
37
+ @property({ type: Boolean })
38
+ readonly: boolean = false
39
+
40
+ @property({ type: Boolean })
41
+ disabled: boolean = false
42
+
43
+ @property({ type: Object })
44
+ inputClasses: Record<string, boolean> = {}
45
+
46
+ @property({ type: Object })
47
+ internals?: ElementInternals
48
+
49
+ // Abstract property - must be implemented by subclasses
50
+ abstract strings: IDatepickerStrings
51
+
52
+ // Shared refs
53
+ inputRef: Ref<HTMLInputElement> = createRef()
54
+ btnRef: Ref<HTMLButtonElement> = createRef()
55
+
56
+ // Shared getters
57
+ get inputElement(): HTMLInputElement | undefined {
58
+ return this.inputRef.value
59
+ }
60
+
61
+ get buttonElement(): HTMLButtonElement | undefined {
62
+ return this.btnRef.value
63
+ }
64
+
65
+ get isInputReadonly(): boolean {
66
+ return this.readonly || this.inputType === 'text'
67
+ }
68
+
69
+ // Shared event dispatchers (protected so subclasses can use them)
70
+ protected dispatchToggleCalendar(e: Event): void {
71
+ if (this.readonly) return
72
+
73
+ this.dispatchEvent(
74
+ new CustomEvent('toggle-calendar', {
75
+ detail: e,
76
+ bubbles: true,
77
+ composed: true,
78
+ }),
79
+ )
80
+ }
81
+
82
+ protected dispatchInput(e: Event): void {
83
+ this.dispatchEvent(
84
+ new CustomEvent('input-change', {
85
+ detail: e,
86
+ bubbles: true,
87
+ composed: true,
88
+ }),
89
+ )
90
+ }
91
+
92
+ protected dispatchFocus(): void {
93
+ this.dispatchEvent(
94
+ new CustomEvent('input-focus', {
95
+ bubbles: true,
96
+ composed: true,
97
+ }),
98
+ )
99
+ }
100
+
101
+ protected dispatchBlur(e: FocusEvent): void {
102
+ this.dispatchEvent(
103
+ new CustomEvent('input-blur', {
104
+ detail: e,
105
+ bubbles: true,
106
+ composed: true,
107
+ }),
108
+ )
109
+ }
110
+
111
+ protected dispatchChange(e: Event): void {
112
+ this.dispatchEvent(
113
+ new CustomEvent('input-changed', {
114
+ detail: e,
115
+ bubbles: true,
116
+ composed: true,
117
+ }),
118
+ )
119
+ }
120
+
121
+ // Shared render helper for calendar button
122
+ protected renderCalendarButton(): TemplateResult {
123
+ return html`
124
+ <button
125
+ class="${classMap(cssUtils.getButtonClasses())}"
126
+ type="button"
127
+ @click=${(e: Event) => this.dispatchToggleCalendar(e)}
128
+ @keydown=${(e: KeyboardEvent) => {
129
+ const { key } = e
130
+ if (key === 'Enter' || key === ' ' || key === 'Space') {
131
+ e.preventDefault()
132
+ this.dispatchToggleCalendar(e)
133
+ }
134
+ }}
135
+ ?disabled=${this.disabled}
136
+ ${this.btnRef}
137
+ >
138
+ <pkt-icon name="calendar"></pkt-icon>
139
+ <span class="pkt-btn__text">${this.strings.calendar?.buttonAltText || 'Åpne kalender'}</span>
140
+ </button>
141
+ `
142
+ }
143
+
144
+ // Shared method - no shadow DOM
145
+ createRenderRoot() {
146
+ return this
147
+ }
148
+
149
+ // Abstract render method - must be implemented by subclasses
150
+ abstract render(): TemplateResult
151
+ }