@oslokommune/punkt-elements 13.6.5 → 13.6.7

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.
@@ -1,7 +1,7 @@
1
1
  import { classMap } from 'lit/directives/class-map.js'
2
2
  import { ifDefined } from 'lit/directives/if-defined.js'
3
3
  import { customElement, property, state } from 'lit/decorators.js'
4
- import { formatISODate, fromISOToDate, newDate, parseISODateString } from '@/utils/dateutils'
4
+ import { formatISODate, fromISOToDate, parseISODateString } from '@/utils/dateutils'
5
5
  import { html, nothing, PropertyValues } from 'lit'
6
6
  import { PktCalendar } from '@/components/calendar/calendar'
7
7
  import { PktInputElement } from '@/base-elements/input-element'
@@ -13,14 +13,26 @@ import '@/components/icon'
13
13
  import '@/components/input-wrapper'
14
14
  import './date-tags'
15
15
  import { PktSlotController } from '@/controllers/pkt-slot-controller'
16
-
17
- const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
16
+ import { keyboardUtils } from './datepicker-utils'
17
+ import {
18
+ sleep,
19
+ deviceDetection,
20
+ valueUtils,
21
+ inputTypeUtils,
22
+ calendarUtils,
23
+ eventUtils,
24
+ cssUtils,
25
+ dateProcessingUtils,
26
+ formUtils,
27
+ } from './datepicker-utils'
18
28
  @customElement('pkt-datepicker')
19
29
  export class PktDatepicker extends PktInputElement {
20
30
  /**
21
31
  * Element attributes and properties
22
32
  */
23
33
  private _valueProperty: string = ''
34
+ private documentClickListener?: (e: MouseEvent) => void
35
+ private documentKeydownListener?: (e: KeyboardEvent) => void
24
36
 
25
37
  @property({ type: String, reflect: true })
26
38
  get value(): string {
@@ -85,6 +97,13 @@ export class PktDatepicker extends PktInputElement {
85
97
  @state() inputClasses = {}
86
98
  @state() buttonClasses = {}
87
99
 
100
+ /**
101
+ * Computed properties
102
+ */
103
+ get inputType(): string {
104
+ return inputTypeUtils.getInputType()
105
+ }
106
+
88
107
  /**
89
108
  * Housekeeping / lifecycle methods
90
109
  */
@@ -94,98 +113,71 @@ export class PktDatepicker extends PktInputElement {
94
113
  this.slotController = new PktSlotController(this, this.helptextSlot)
95
114
  }
96
115
 
97
- async connectedCallback() {
116
+ connectedCallback() {
98
117
  super.connectedCallback()
99
-
100
- const ua = navigator.userAgent
101
- const isIOS = /iP(hone|od|ad)/.test(ua)
102
-
103
- this.inputType = isIOS ? 'text' : 'date'
104
-
105
- document &&
106
- document.body.addEventListener('click', (e: MouseEvent) => {
107
- if (
108
- this.inputRef?.value &&
109
- this.btnRef?.value &&
110
- !this.inputRef.value.contains(e.target as Node) &&
111
- !(this.inputRefTo.value && this.inputRefTo.value.contains(e.target as Node)) &&
112
- !this.btnRef.value.contains(e.target as Node) &&
113
- !(e.target as Element).closest('.pkt-calendar-popup') &&
114
- this.calendarOpen
115
- ) {
116
- this.onBlur()
117
- this.hideCalendar()
118
- }
119
- })
120
-
121
- document &&
122
- document.body.addEventListener('keydown', (e: KeyboardEvent) => {
123
- if (e.key === 'Escape' && this.calendarOpen) {
124
- this.hideCalendar()
125
- }
126
- })
127
-
128
- if (this.value) {
129
- this._value = Array.isArray(this.value)
130
- ? this.value.filter(Boolean)
131
- : this.value.split(',').filter(Boolean)
132
- }
133
- this.min = this.min || specs.props.min.default
134
- this.max = this.max || specs.props.max.default
135
-
136
- if (typeof this.excludedates === 'string') {
137
- this.excludedates = (this.excludedates as unknown as string).split(',')
138
- }
139
-
140
- if (typeof this.excludeweekdays === 'string') {
141
- this.excludeweekdays = (this.excludeweekdays as unknown as string).split(',')
142
- }
143
-
144
- if ((this.multiple || this.range) && this.name && !this.name.endsWith('[]')) {
145
- this.name = this.name + '[]'
146
- }
147
-
148
- if (this.calendarOpen) {
149
- await sleep(20)
150
- this.handleCalendarPosition()
118
+ if (this.timezone && this.timezone !== window.pktTz) {
119
+ window.pktTz = this.timezone
151
120
  }
121
+ this.name =
122
+ valueUtils.normalizeNameForMultiple(this.name, this.multiple, this.range) || this.name
123
+ this.documentClickListener = eventUtils.createDocumentClickListener(
124
+ this.inputRef,
125
+ this.inputRefTo,
126
+ this.btnRef,
127
+ () => this.calendarOpen,
128
+ this.onBlur.bind(this),
129
+ this.hideCalendar.bind(this),
130
+ )
131
+ this.documentKeydownListener = eventUtils.createDocumentKeydownListener(
132
+ () => this.calendarOpen,
133
+ this.hideCalendar.bind(this),
134
+ )
135
+ document.addEventListener('click', this.documentClickListener)
136
+ document.addEventListener('keydown', this.documentKeydownListener)
152
137
  }
153
138
 
154
139
  disconnectedCallback(): void {
155
140
  super.disconnectedCallback()
156
- document &&
157
- document.body.removeEventListener('click', (e: MouseEvent) => {
158
- if (
159
- this.inputRef?.value &&
160
- this.btnRef?.value &&
161
- !this.inputRef.value.contains(e.target as Node) &&
162
- !this.btnRef.value.contains(e.target as Node)
163
- ) {
164
- this.hideCalendar()
165
- }
166
- })
141
+ if (this.documentClickListener) {
142
+ document.removeEventListener('click', this.documentClickListener)
143
+ }
144
+ if (this.documentKeydownListener) {
145
+ document.removeEventListener('keydown', this.documentKeydownListener)
146
+ }
167
147
  }
168
148
 
169
149
  onInput(): void {
170
- // Trigger input event for form validation
171
150
  this.dispatchEvent(new Event('input', { bubbles: true }))
172
151
  }
173
152
 
174
153
  valueChanged(newValue: string | null, oldValue: string | null): void {
175
154
  if (newValue === oldValue) return
176
155
 
177
- let parsedValue: string[] = []
178
- if (newValue) {
179
- if (typeof newValue === 'string') {
180
- parsedValue = newValue.split(',').filter(Boolean)
181
- } else {
182
- parsedValue = String(newValue).split(',').filter(Boolean)
183
- }
156
+ const parsedValue = valueUtils.parseValue(newValue)
157
+
158
+ // For multiple dates, filter out invalid ones to prevent accumulating bad dates
159
+ // For single/range dates, preserve user input for validation feedback
160
+ const filteredValue =
161
+ this.multiple && parsedValue.length > 1
162
+ ? valueUtils.filterSelectableDates(
163
+ parsedValue,
164
+ this.min,
165
+ this.max,
166
+ this.excludedates,
167
+ this.excludeweekdays,
168
+ )
169
+ : parsedValue
170
+
171
+ if (this.range && !valueUtils.validateRangeOrder(filteredValue)) {
172
+ this._value = []
173
+ this._valueProperty = ''
174
+ super.valueChanged('', oldValue)
175
+ return
184
176
  }
185
177
 
186
- this._value = parsedValue
178
+ this._value = filteredValue
187
179
 
188
- const parsedValueString = parsedValue.join(',')
180
+ const parsedValueString = valueUtils.formatValue(filteredValue)
189
181
  if (this._valueProperty !== parsedValueString) {
190
182
  this._valueProperty = parsedValueString
191
183
  }
@@ -199,11 +191,11 @@ export class PktDatepicker extends PktInputElement {
199
191
  }
200
192
 
201
193
  if (name === 'excludedates' && typeof this.excludedates === 'string') {
202
- this.excludedates = value?.split(',') ?? []
194
+ this.excludedates = valueUtils.normalizeStringArray(value || '')
203
195
  }
204
196
 
205
197
  if (name === 'excludeweekdays' && typeof this.excludeweekdays === 'string') {
206
- this.excludeweekdays = value?.split(',') ?? []
198
+ this.excludeweekdays = valueUtils.normalizeStringArray(value || '')
207
199
  }
208
200
  super.attributeChangedCallback(name, _old, value)
209
201
  }
@@ -215,19 +207,16 @@ export class PktDatepicker extends PktInputElement {
215
207
  const oldValueStr = Array.isArray(oldValue) ? oldValue.join(',') : oldValue
216
208
  this.valueChanged(newValue, oldValueStr)
217
209
  }
210
+ if (changedProperties.has('multiple') || changedProperties.has('range')) {
211
+ this.name =
212
+ valueUtils.normalizeNameForMultiple(this.name, this.multiple, this.range) || this.name
213
+ }
218
214
  if (changedProperties.has('multiple')) {
219
- // If multiple is now true, ensure _value is an array of non-empty strings
220
215
  if (this.multiple && !Array.isArray(this._value)) {
221
- this._value =
222
- typeof this.value === 'string'
223
- ? this.value
224
- ? this.value.split(',').filter(Boolean)
225
- : []
226
- : []
216
+ this._value = valueUtils.parseValue(this.value)
227
217
  } else if (!this.multiple && Array.isArray(this._value)) {
228
218
  this._value = this._value.filter(Boolean)
229
219
  }
230
- // If multiple is now false, ensure _value is a single value (but not for range datepickers)
231
220
  if (!this.multiple && !this.range && Array.isArray(this._value)) {
232
221
  this._value = [this._value[0] ?? '']
233
222
  }
@@ -267,30 +256,22 @@ export class PktDatepicker extends PktInputElement {
267
256
  this.showCalendar()
268
257
  }}
269
258
  ?disabled=${this.disabled}
270
- @keydown=${(e: KeyboardEvent) => {
271
- if (e.key === ',') {
272
- this.inputRef.value?.blur()
273
- }
274
- if (e.key === 'Space' || e.key === ' ') {
275
- e.preventDefault()
276
- this.toggleCalendar(e)
277
- }
278
- if (e.key === 'Enter') {
279
- const form = this.internals.form as HTMLFormElement
280
- if (form) {
281
- form.requestSubmit()
282
- } else {
283
- this.inputRef.value?.blur()
284
- }
285
- }
286
- }}
259
+ @keydown=${(e: KeyboardEvent) =>
260
+ keyboardUtils.handleInputKeydown(
261
+ // event, toggleCalendar, submitForm, focusNext, blur, comma
262
+ e,
263
+ (event) => this.toggleCalendar(event),
264
+ () => formUtils.submitFormOrFallback(this.internals, () => this.inputRef.value?.blur()),
265
+ undefined,
266
+ () => this.inputRef.value?.blur(),
267
+ )}
287
268
  @input=${(e: Event) => {
288
269
  this.onInput()
289
270
  e.stopImmediatePropagation()
290
271
  }}
291
272
  @focus=${() => {
292
273
  this.onFocus()
293
- if (this.isMobileSafari) {
274
+ if (deviceDetection.isMobileSafari()) {
294
275
  this.showCalendar()
295
276
  }
296
277
  }}
@@ -311,10 +292,7 @@ export class PktDatepicker extends PktInputElement {
311
292
  }
312
293
 
313
294
  renderRangeInput() {
314
- const rangeLabelClasses = {
315
- 'pkt-input-prefix': this.showRangeLabels,
316
- 'pkt-hide': !this.showRangeLabels,
317
- }
295
+ const rangeLabelClasses = cssUtils.getRangeLabelClasses(this.showRangeLabels)
318
296
  return html`
319
297
  ${this.showRangeLabels
320
298
  ? html` <div class="pkt-input-prefix">${this.strings.generic.from}</div> `
@@ -333,46 +311,34 @@ export class PktDatepicker extends PktInputElement {
333
311
  e.preventDefault()
334
312
  this.showCalendar()
335
313
  }}
336
- @keydown=${(e: KeyboardEvent) => {
337
- if (e.key === ',') {
338
- this.inputRef.value?.blur()
339
- }
340
- if (e.key === 'Space' || e.key === ' ') {
341
- e.preventDefault()
342
- this.toggleCalendar(e)
343
- }
344
- if (e.key === 'Enter') {
345
- const form = this.internals.form as HTMLFormElement
346
- if (form) {
347
- form.requestSubmit()
348
- } else {
349
- this.inputRefTo.value?.focus()
350
- }
351
- }
352
- }}
314
+ @keydown=${(e: KeyboardEvent) =>
315
+ keyboardUtils.handleInputKeydown(
316
+ // event, toggleCalendar, submitForm, focusNext, blur, comma
317
+ e,
318
+ (event) => this.toggleCalendar(event),
319
+ () =>
320
+ formUtils.submitFormOrFallback(this.internals, () => this.inputRefTo.value?.focus()),
321
+ () => this.inputRefTo.value?.focus(),
322
+ () => this.inputRef.value?.blur(),
323
+ )}
353
324
  @input=${(e: Event) => {
354
325
  this.onInput()
355
326
  e.stopImmediatePropagation()
356
327
  }}
357
328
  @focus=${() => {
358
329
  this.onFocus()
359
- if (this.isMobileSafari) {
330
+ if (deviceDetection.isMobileSafari()) {
360
331
  this.showCalendar()
361
332
  }
362
333
  }}
363
334
  @blur=${(e: Event) => {
364
- if ((e.target as HTMLInputElement).value) {
365
- this.manageValidity(e.target as HTMLInputElement)
366
- const date = fromISOToDate((e.target as HTMLInputElement).value)
367
- if (date) {
368
- if (this._value[0] !== (e.target as HTMLInputElement).value && this._value[1]) {
369
- this.clearInputValue()
370
- this.calRef?.value?.handleDateSelect(date)
371
- }
372
- }
373
- } else if (this._value[0]) {
374
- this.clearInputValue()
375
- }
335
+ dateProcessingUtils.processRangeBlur(
336
+ e,
337
+ this._value,
338
+ this.calRef,
339
+ () => this.clearInputValue(),
340
+ (input) => this.manageValidity(input),
341
+ )
376
342
  }}
377
343
  @change=${(e: Event) => {
378
344
  e.stopImmediatePropagation()
@@ -398,30 +364,23 @@ export class PktDatepicker extends PktInputElement {
398
364
  e.preventDefault()
399
365
  this.showCalendar()
400
366
  }}
401
- @keydown=${(e: KeyboardEvent) => {
402
- if (e.key === ',') {
403
- this.inputRefTo.value?.blur()
404
- }
405
- if (e.key === 'Space' || e.key === ' ') {
406
- e.preventDefault()
407
- this.toggleCalendar(e)
408
- }
409
- if (e.key === 'Enter') {
410
- const form = this.internals.form as HTMLFormElement
411
- if (form) {
412
- form.requestSubmit()
413
- } else {
414
- this.inputRefTo.value?.blur()
415
- }
416
- }
417
- }}
367
+ @keydown=${(e: KeyboardEvent) =>
368
+ keyboardUtils.handleInputKeydown(
369
+ // event, toggleCalendar, submitForm, focusNext, blur, comma
370
+ e,
371
+ (event) => this.toggleCalendar(event),
372
+ () =>
373
+ formUtils.submitFormOrFallback(this.internals, () => this.inputRefTo.value?.blur()),
374
+ undefined,
375
+ () => this.inputRefTo.value?.blur(),
376
+ )}
418
377
  @input=${(e: Event) => {
419
378
  this.onInput()
420
379
  e.stopImmediatePropagation()
421
380
  }}
422
381
  @focus=${() => {
423
382
  this.onFocus()
424
- if (this.isMobileSafari) {
383
+ if (deviceDetection.isMobileSafari()) {
425
384
  this.showCalendar()
426
385
  }
427
386
  }}
@@ -431,20 +390,13 @@ export class PktDatepicker extends PktInputElement {
431
390
  }
432
391
  if ((e.target as HTMLInputElement).value) {
433
392
  this.manageValidity(e.target as HTMLInputElement)
434
- const val = (e.target as HTMLInputElement).value
435
- if (this.min && this.min > val) {
436
- this.internals.setValidity(
437
- { rangeUnderflow: true },
438
- this.strings.forms.messages.rangeUnderflow,
439
- e.target as HTMLInputElement,
440
- )
441
- } else if (this.max && this.max < val) {
442
- this.internals.setValidity(
443
- { rangeOverflow: true },
444
- this.strings.forms.messages.rangeOverflow,
445
- e.target as HTMLInputElement,
446
- )
447
- }
393
+ formUtils.validateDateInput(
394
+ e.target as HTMLInputElement,
395
+ this.internals,
396
+ this.min,
397
+ this.max,
398
+ this.strings,
399
+ )
448
400
  const date = fromISOToDate((e.target as HTMLInputElement).value)
449
401
  if (date) {
450
402
  if (this._value[1] !== formatISODate(date)) {
@@ -489,28 +441,20 @@ export class PktDatepicker extends PktInputElement {
489
441
  }}
490
442
  @focus=${() => {
491
443
  this.onFocus()
492
- if (this.isMobileSafari) {
444
+ if (deviceDetection.isMobileSafari()) {
493
445
  this.showCalendar()
494
446
  }
495
447
  }}
496
- @keydown=${(e: KeyboardEvent) => {
497
- if (e.key === ',') {
498
- e.preventDefault()
499
- this.addToSelected(e)
500
- }
501
- if (e.key === 'Space' || e.key === ' ') {
502
- e.preventDefault()
503
- this.toggleCalendar(e)
504
- }
505
- if (e.key === 'Enter') {
506
- const form = this.internals.form as HTMLFormElement
507
- if (form) {
508
- form.requestSubmit()
509
- } else {
510
- this.inputRef.value?.blur()
511
- }
512
- }
513
- }}
448
+ @keydown=${(e: KeyboardEvent) =>
449
+ keyboardUtils.handleInputKeydown(
450
+ // event, toggleCalendar, submitForm, focusNext, blur, comma
451
+ e,
452
+ (event) => this.toggleCalendar(event),
453
+ () => formUtils.submitFormOrFallback(this.internals, () => this.inputRef.value?.blur()),
454
+ undefined,
455
+ undefined,
456
+ (event) => this.addToSelected(event),
457
+ )}
514
458
  @change=${(e: Event) => {
515
459
  this.touched = true
516
460
  e.stopImmediatePropagation()
@@ -545,26 +489,16 @@ export class PktDatepicker extends PktInputElement {
545
489
  .excludeweekdays=${this.excludeweekdays}
546
490
  .currentmonth=${this.currentmonth ? parseISODateString(this.currentmonth) : null}
547
491
  @date-selected=${(e: CustomEvent) => {
548
- this.value =
549
- !this.multiple && !this.range
550
- ? e.detail[0]
551
- : Array.isArray(e.detail)
552
- ? e.detail.join(',')
553
- : e.detail
492
+ this.value = dateProcessingUtils.processDateSelection(e.detail, this.multiple, this.range)
554
493
  this._value = e.detail
555
- if (this.inputRef.value) {
556
- if (this.range && this.inputRefTo.value) {
557
- this.inputRef.value.value = this._value[0] ?? ''
558
- this.inputRefTo.value.value = this._value[1] ?? ''
559
- // Update validity state after programmatic value change
560
- this.manageValidity(this.inputRef.value)
561
- this.manageValidity(this.inputRefTo.value)
562
- } else if (!this.multiple) {
563
- this.inputRef.value.value = this._value.length ? this._value[0] : ''
564
- // Update validity state after programmatic value change
565
- this.manageValidity(this.inputRef.value)
566
- }
567
- }
494
+ dateProcessingUtils.updateInputValues(
495
+ this.inputRef,
496
+ this.inputRefTo,
497
+ this._value,
498
+ this.range,
499
+ this.multiple,
500
+ (input) => this.manageValidity(input),
501
+ )
568
502
  }}
569
503
  @close=${() => {
570
504
  this.onBlur()
@@ -576,22 +510,13 @@ export class PktDatepicker extends PktInputElement {
576
510
  }
577
511
 
578
512
  render() {
579
- this.inputClasses = {
580
- 'pkt-input': true,
581
- 'pkt-datepicker__input': true,
582
- 'pkt-input--fullwidth': this.fullwidth,
583
- 'pkt-datepicker--hasrangelabels': this.showRangeLabels,
584
- 'pkt-datepicker--multiple': this.multiple,
585
- 'pkt-datepicker--range': this.range,
586
- }
587
-
588
- this.buttonClasses = {
589
- 'pkt-input-icon': true,
590
- 'pkt-btn': true,
591
- 'pkt-btn--icon-only': true,
592
- 'pkt-btn--tertiary': true,
593
- 'pkt-datepicker__calendar-button': true,
594
- }
513
+ this.inputClasses = cssUtils.getInputClasses(
514
+ this.fullwidth,
515
+ this.showRangeLabels,
516
+ this.multiple,
517
+ this.range,
518
+ )
519
+ this.buttonClasses = cssUtils.getButtonClasses()
595
520
 
596
521
  return html`
597
522
  <pkt-input-wrapper
@@ -645,12 +570,8 @@ export class PktDatepicker extends PktInputElement {
645
570
  class="${classMap(this.buttonClasses)}"
646
571
  type="button"
647
572
  @click=${this.toggleCalendar}
648
- @keydown=${(e: KeyboardEvent) => {
649
- if (e.key === 'Enter' || e.key === ' ' || e.key === 'Space') {
650
- e.preventDefault()
651
- this.toggleCalendar(e)
652
- }
653
- }}
573
+ @keydown=${(e: KeyboardEvent) =>
574
+ keyboardUtils.handleButtonKeydown(e, (event) => this.toggleCalendar(event))}
654
575
  ?disabled=${this.disabled}
655
576
  ${ref(this.btnRef)}
656
577
  >
@@ -669,58 +590,23 @@ export class PktDatepicker extends PktInputElement {
669
590
  */
670
591
 
671
592
  handleCalendarPosition() {
672
- if (this.popupRef.value && this.inputRef.value) {
673
- const counter = this.multiple && !!this.maxlength
674
-
675
- const inputRect =
676
- this.inputRef.value.parentElement?.getBoundingClientRect() ||
677
- this.inputRef.value.getBoundingClientRect()
678
-
679
- const inputHeight = counter ? inputRect.height + 30 : inputRect.height
680
- const popupHeight = this.popupRef.value.getBoundingClientRect().height
681
-
682
- let top = counter ? 'calc(100% - 30px)' : '100%'
683
- if (
684
- inputRect &&
685
- inputRect.top + popupHeight > window.innerHeight &&
686
- inputRect.top - popupHeight > 0
687
- ) {
688
- top = `calc(100% - ${inputHeight}px - ${popupHeight}px)`
689
- }
690
- this.popupRef.value.style.top = top
691
- }
593
+ const hasCounter = this.multiple && !!this.maxlength
594
+ calendarUtils.handleCalendarPosition(this.popupRef, this.inputRef, hasCounter)
692
595
  }
693
596
 
694
597
  addToSelected = (e: Event | KeyboardEvent) => {
695
- const target = e.target as HTMLInputElement
696
- if (!target.value) return
697
- const minAsDate = this.min ? newDate(this.min as string) : null
698
- const maxAsDate = this.max ? newDate(this.max as string) : null
699
- const date = newDate(target.value.split(',')[0])
700
- if (
701
- date &&
702
- !isNaN(date.getTime()) &&
703
- (!minAsDate || date >= minAsDate) &&
704
- (!maxAsDate || date <= maxAsDate) &&
705
- this.calRef.value
706
- ) {
707
- this.calRef.value.handleDateSelect(date)
708
- }
709
- target.value = ''
598
+ calendarUtils.addToSelected(e, this.calRef, this.min, this.max)
710
599
  }
711
600
 
712
601
  private handleFocusOut(e: FocusEvent) {
713
- if (!this.contains(e.target as Node)) {
714
- this.onBlur()
715
- this.hideCalendar()
716
- }
602
+ eventUtils.handleFocusOut(e, this, this.onBlur.bind(this), this.hideCalendar.bind(this))
717
603
  }
718
604
 
719
605
  public async showCalendar() {
720
606
  this.calendarOpen = true
721
607
  await sleep(20)
722
608
  this.handleCalendarPosition()
723
- if (this.isMobileSafari) {
609
+ if (deviceDetection.isMobileSafari()) {
724
610
  this.calRef.value?.focusOnCurrentDate()
725
611
  }
726
612
  }