@oslokommune/punkt-elements 14.0.2 → 14.0.4
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/CHANGELOG.md +17 -0
- package/dist/calendar-BtShW7ER.cjs +90 -0
- package/dist/{calendar-Bz27nuTP.js → calendar-yxjSI4wd.js} +766 -682
- package/dist/datepicker-D0q75U1Z.js +1463 -0
- package/dist/datepicker-DDV382Uu.cjs +271 -0
- package/dist/index.d.ts +118 -83
- package/dist/pkt-calendar.cjs +1 -1
- package/dist/pkt-calendar.js +1 -1
- package/dist/pkt-datepicker.cjs +1 -1
- package/dist/pkt-datepicker.js +2 -2
- package/dist/pkt-index.cjs +1 -1
- package/dist/pkt-index.js +3 -3
- package/package.json +4 -4
- package/src/components/calendar/calendar.ts +372 -414
- package/src/components/calendar/helpers/calendar-grid.ts +93 -0
- package/src/components/calendar/helpers/date-validation.ts +86 -0
- package/src/components/calendar/helpers/index.ts +49 -0
- package/src/components/calendar/helpers/keyboard-navigation.ts +54 -0
- package/src/components/calendar/helpers/selection-manager.ts +184 -0
- package/src/components/datepicker/datepicker-base.ts +151 -0
- package/src/components/datepicker/datepicker-multiple.ts +7 -114
- package/src/components/datepicker/datepicker-range.ts +21 -141
- package/src/components/datepicker/datepicker-single.ts +7 -115
- package/src/components/datepicker/datepicker-types.ts +56 -0
- package/src/components/datepicker/datepicker-utils.test.ts +730 -0
- package/src/components/datepicker/datepicker-utils.ts +338 -9
- package/src/components/datepicker/datepicker.ts +25 -1
- package/dist/calendar-Dz1Cnzx5.cjs +0 -115
- package/dist/datepicker-CnCOXI2x.cjs +0 -289
- 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
|
+
}
|