@oslokommune/punkt-elements 12.31.2 → 12.32.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.
Files changed (181) hide show
  1. package/dist/alert-C2WKLFow.cjs +27 -0
  2. package/dist/alert-XGjypFHe.js +115 -0
  3. package/dist/alert.d.ts +32 -0
  4. package/dist/{backlink-CzpB-ih9.js → backlink-BX8HBhw5.js} +2 -2
  5. package/dist/{backlink-Dn4DfWVR.cjs → backlink-NJdC0FAO.cjs} +1 -1
  6. package/dist/backlink.d.ts +32 -0
  7. package/dist/{button-9NwGr-OS.js → button-Bn8jVB0f.js} +33 -33
  8. package/dist/{button-Beo3c7cx.cjs → button-Bpbh8A_a.cjs} +2 -2
  9. package/dist/button.d.ts +32 -0
  10. package/dist/{calendar-BbZNxsKY.js → calendar-BNHDEEwT.js} +20 -84
  11. package/dist/{calendar-CxBo98iI.cjs → calendar-Ds13AKr5.cjs} +4 -4
  12. package/dist/calendar.d.ts +32 -0
  13. package/dist/card-CEv3DL-o.js +94 -0
  14. package/dist/card-c5EEPaNh.cjs +28 -0
  15. package/dist/card.d.ts +32 -0
  16. package/dist/{checkbox-9Zjy_NU7.cjs → checkbox-DDBBDM85.cjs} +1 -1
  17. package/dist/{checkbox-CzDpR6_8.js → checkbox-DoMYxp83.js} +9 -9
  18. package/dist/checkbox.d.ts +32 -0
  19. package/dist/{class-map-DWtqmIRS.js → class-map-D4tSZX70.js} +2 -2
  20. package/dist/{class-map-Dj5mbCUg.cjs → class-map-DvBKxTiW.cjs} +1 -1
  21. package/dist/combobox-BflWORVa.cjs +115 -0
  22. package/dist/combobox-l7LmWQYu.js +491 -0
  23. package/dist/combobox.d.ts +47 -0
  24. package/dist/datepicker-Bkiyh5P5.cjs +153 -0
  25. package/dist/datepicker-nWd89NwP.js +449 -0
  26. package/dist/datepicker.d.ts +32 -0
  27. package/dist/directive-C7oCP5Bh.cjs +5 -0
  28. package/dist/directive-helpers-BmXIK-1b.js +45 -0
  29. package/dist/directive-helpers-donnvcEO.cjs +5 -0
  30. package/dist/{directive-B76A7YXI.js → directive-oAbCiebi.js} +4 -4
  31. package/dist/element-DJpGVXjq.cjs +238 -0
  32. package/dist/{element-C7XjZtLU.js → element-DiqKrDty.js} +8851 -8847
  33. package/dist/{helptext-CqnoPodd.js → helptext-DfRusn5R.js} +13 -13
  34. package/dist/{helptext-B9kxDc2b.cjs → helptext-aHD9icur.cjs} +2 -2
  35. package/dist/helptext.d.ts +32 -0
  36. package/dist/{icon-BEUgV9Wo.js → icon-BdY52BIt.js} +2 -2
  37. package/dist/{icon-BOKusjAA.cjs → icon-UtDKPH3R.cjs} +1 -1
  38. package/dist/icon.d.ts +32 -0
  39. package/dist/{if-defined-CpIkv1A4.cjs → if-defined-CoBDkDGb.cjs} +1 -1
  40. package/dist/{if-defined-eRX4e5zO.js → if-defined-DeANa4n5.js} +1 -1
  41. package/dist/index.d.ts +210 -15
  42. package/dist/{input-element-BK8UkQli.js → input-element-C4mmTk6h.js} +23 -20
  43. package/dist/input-element-VNai8bsq.cjs +1 -0
  44. package/dist/input-wrapper-8iKEzDRZ.js +40 -0
  45. package/dist/input-wrapper-DWOw7Yn-.cjs +52 -0
  46. package/dist/input-wrapper-EoSAbU-U.cjs +1 -0
  47. package/dist/input-wrapper-P_oEi6dk.js +185 -0
  48. package/dist/input-wrapper.d.ts +32 -0
  49. package/dist/link-D9dJ6F-R.js +73 -0
  50. package/dist/link-DBxXqsnP.cjs +8 -0
  51. package/dist/link.d.ts +32 -0
  52. package/dist/{linkcard-2WzDJPZz.cjs → linkcard-D5O1hR7N.cjs} +1 -1
  53. package/dist/{linkcard-CRpo3tiw.js → linkcard-DcEE9IVw.js} +4 -4
  54. package/dist/linkcard.d.ts +32 -0
  55. package/dist/listbox-CK1GmwCE.cjs +95 -0
  56. package/dist/listbox-CfJWJDrc.js +360 -0
  57. package/dist/listbox.d.ts +47 -0
  58. package/dist/{loader-B1edLWTg.js → loader-DZvOlXzI.js} +6 -6
  59. package/dist/{loader-DI74pe25.cjs → loader-SDXJp58K.cjs} +1 -1
  60. package/dist/loader.d.ts +32 -0
  61. package/dist/messagebox-Ce5e1gga.js +79 -0
  62. package/dist/messagebox-D2_-WZQG.cjs +12 -0
  63. package/dist/messagebox.d.ts +32 -0
  64. package/dist/modal-CCtUsYKk.cjs +33 -0
  65. package/dist/{modal-kPX8nO_L.js → modal-CdgIOp8Q.js} +51 -94
  66. package/dist/modal.d.ts +32 -0
  67. package/dist/pkt-alert.cjs +1 -1
  68. package/dist/pkt-alert.js +1 -1
  69. package/dist/pkt-backlink.cjs +1 -1
  70. package/dist/pkt-backlink.js +1 -1
  71. package/dist/pkt-button.cjs +1 -1
  72. package/dist/pkt-button.js +1 -1
  73. package/dist/pkt-calendar.cjs +1 -1
  74. package/dist/pkt-calendar.js +1 -1
  75. package/dist/pkt-card.cjs +1 -1
  76. package/dist/pkt-card.js +1 -1
  77. package/dist/pkt-checkbox.cjs +1 -1
  78. package/dist/pkt-checkbox.js +1 -1
  79. package/dist/pkt-combobox.cjs +1 -0
  80. package/dist/pkt-combobox.js +6 -0
  81. package/dist/pkt-datepicker.cjs +1 -1
  82. package/dist/pkt-datepicker.js +1 -1
  83. package/dist/pkt-helptext.cjs +1 -1
  84. package/dist/pkt-helptext.js +1 -1
  85. package/dist/pkt-icon.cjs +1 -1
  86. package/dist/pkt-icon.js +1 -1
  87. package/dist/pkt-index.cjs +3 -3
  88. package/dist/pkt-index.js +45 -43
  89. package/dist/pkt-input-wrapper.cjs +1 -1
  90. package/dist/pkt-input-wrapper.js +1 -1
  91. package/dist/pkt-link.cjs +1 -1
  92. package/dist/pkt-link.js +1 -1
  93. package/dist/pkt-linkcard.cjs +1 -1
  94. package/dist/pkt-linkcard.js +1 -1
  95. package/dist/pkt-listbox.cjs +1 -0
  96. package/dist/pkt-listbox.js +6 -0
  97. package/dist/pkt-loader.cjs +1 -1
  98. package/dist/pkt-loader.js +1 -1
  99. package/dist/pkt-messagebox.cjs +1 -1
  100. package/dist/pkt-messagebox.js +1 -1
  101. package/dist/pkt-modal.cjs +1 -1
  102. package/dist/pkt-modal.js +1 -1
  103. package/dist/pkt-options-controller-BkWE4pZe.js +38 -0
  104. package/dist/pkt-options-controller-TJ1apYGF.cjs +1 -0
  105. package/dist/pkt-progressbar.cjs +1 -1
  106. package/dist/pkt-progressbar.js +2 -2
  107. package/dist/pkt-radiobutton.cjs +1 -1
  108. package/dist/pkt-radiobutton.js +1 -1
  109. package/dist/pkt-select.cjs +1 -1
  110. package/dist/pkt-select.js +1 -1
  111. package/dist/pkt-slot-controller-BtVLTZb7.js +79 -0
  112. package/dist/pkt-slot-controller-plQxXRvV.cjs +1 -0
  113. package/dist/pkt-tag.cjs +1 -1
  114. package/dist/pkt-tag.js +1 -1
  115. package/dist/pkt-textarea.cjs +1 -1
  116. package/dist/pkt-textarea.js +1 -1
  117. package/dist/pkt-textinput.cjs +1 -1
  118. package/dist/pkt-textinput.js +1 -1
  119. package/dist/{progressbar-D0nxLqHu.js → progressbar-DvFz9KBo.js} +6 -6
  120. package/dist/{progressbar-B6A9UVXS.cjs → progressbar-jASTnhti.cjs} +1 -1
  121. package/dist/progressbar.d.ts +32 -0
  122. package/dist/{radiobutton-BWyQgR_x.cjs → radiobutton-C-Xd5sSq.cjs} +1 -1
  123. package/dist/{radiobutton-DLWjvLBO.js → radiobutton-D03aN4yn.js} +5 -5
  124. package/dist/radiobutton.d.ts +32 -0
  125. package/dist/ref-B70YZNXm.js +102 -0
  126. package/dist/ref-CLqTsViF.cjs +9 -0
  127. package/dist/repeat-CNLpCTun.js +61 -0
  128. package/dist/repeat-t2oms1o6.cjs +5 -0
  129. package/dist/select-BLDaK5Rd.js +118 -0
  130. package/dist/select-Dvds7MzS.cjs +49 -0
  131. package/dist/select.d.ts +32 -0
  132. package/dist/{state-HNj0_316.cjs → state-C4LdlTYO.cjs} +1 -1
  133. package/dist/{state-CDQk0DFQ.js → state-CDwPbiQj.js} +1 -1
  134. package/dist/tag-C52KeBmR.cjs +18 -0
  135. package/dist/{tag-CmFcSdOV.js → tag-DoS5S2Bq.js} +28 -77
  136. package/dist/tag.d.ts +32 -0
  137. package/dist/{textarea-BPqWCymU.js → textarea-C9EvdPGi.js} +18 -17
  138. package/dist/{textarea-VG-UTMLP.cjs → textarea-DJvNBmEL.cjs} +6 -6
  139. package/dist/textarea.d.ts +32 -0
  140. package/dist/{textinput-CEP7QA3E.cjs → textinput-Cg3ObBuw.cjs} +5 -4
  141. package/dist/{textinput-VD74aGzx.js → textinput-CxRSSYaN.js} +34 -33
  142. package/dist/textinput.d.ts +32 -0
  143. package/package.json +4 -4
  144. package/src/components/button/button.ts +3 -2
  145. package/src/components/combobox/combobox.ts +873 -0
  146. package/src/components/combobox/countrycodes.json +927 -0
  147. package/src/components/combobox/index.ts +6 -0
  148. package/src/components/datepicker/datepicker.ts +6 -4
  149. package/src/components/helptext/helptext.ts +1 -1
  150. package/src/components/index.ts +7 -0
  151. package/src/components/input-wrapper/input-wrapper.ts +42 -34
  152. package/src/components/listbox/index.ts +4 -0
  153. package/src/components/listbox/listbox.ts +474 -0
  154. package/src/components/select/select.ts +7 -0
  155. package/src/components/tag/tag.ts +1 -1
  156. package/src/components/textinput/textinput.ts +3 -2
  157. package/dist/alert-BDxxRqgi.cjs +0 -27
  158. package/dist/alert-D0S57u0r.js +0 -163
  159. package/dist/card-C63x_nll.cjs +0 -28
  160. package/dist/card-DQfNKnKl.js +0 -176
  161. package/dist/datepicker-CYUvRGhE.js +0 -663
  162. package/dist/datepicker-FuAL0uNU.cjs +0 -155
  163. package/dist/directive-C_VV3qwo.cjs +0 -5
  164. package/dist/element-CzFXQBoS.cjs +0 -238
  165. package/dist/input-element-Dtyuf6s8.cjs +0 -1
  166. package/dist/input-wrapper-Bo2_t6pA.cjs +0 -50
  167. package/dist/input-wrapper-Bw9tARAD.js +0 -125
  168. package/dist/input-wrapper-DaZZq8c0.js +0 -172
  169. package/dist/input-wrapper-jPnKsfEC.cjs +0 -1
  170. package/dist/link-1iq0Pmuf.cjs +0 -8
  171. package/dist/link-D3U0Jkz8.js +0 -109
  172. package/dist/messagebox-DQpEMkS2.cjs +0 -12
  173. package/dist/messagebox-KP-8-tA9.js +0 -114
  174. package/dist/modal-ytIJwfr3.cjs +0 -33
  175. package/dist/pkt-slot-controller-Clbye6cM.js +0 -85
  176. package/dist/pkt-slot-controller-Oc32unDk.cjs +0 -1
  177. package/dist/ref-2anvRHT4.cjs +0 -13
  178. package/dist/ref-DbOSDQbk.js +0 -143
  179. package/dist/select-CzuxXKll.js +0 -150
  180. package/dist/select-DZL6aa2s.cjs +0 -48
  181. package/dist/tag-BeLSOjNh.cjs +0 -18
@@ -0,0 +1,873 @@
1
+ import { html, nothing, PropertyValues } from 'lit'
2
+ import { ifDefined } from 'lit/directives/if-defined.js'
3
+ import { customElement, property, state } from 'lit/decorators.js'
4
+ import { Ref, createRef, ref } from 'lit/directives/ref.js'
5
+ import { classMap } from 'lit/directives/class-map.js'
6
+ import { repeat } from 'lit/directives/repeat.js'
7
+ import { PktInputElement } from '@/base-elements/input-element'
8
+ import { PktOptionsSlotController } from '@/controllers/pkt-options-controller'
9
+ import { PktSlotController } from '@/controllers/pkt-slot-controller'
10
+ import specs from 'componentSpecs/combobox.json'
11
+
12
+ import '../input-wrapper'
13
+ import '../icon'
14
+ import '../tag'
15
+ import '../listbox'
16
+ import PktListbox from '../listbox'
17
+ import { TTagSkin } from '../tag'
18
+
19
+ export interface IPktComboboxOption {
20
+ description?: string
21
+ disabled?: boolean
22
+ fulltext?: string
23
+ label?: string
24
+ prefix?: string
25
+ selected?: boolean
26
+ tagSkinColor?: TTagSkin
27
+ userAdded?: boolean
28
+ value: string
29
+ }
30
+
31
+ export interface IPktCombobox {
32
+ allowUserInput?: boolean
33
+ typeahead?: boolean
34
+ disabled?: boolean
35
+ displayValueAs?: string
36
+ errorMessage?: string
37
+ fullwidth?: boolean
38
+ hasError?: boolean
39
+ helptext?: string | null
40
+ helptextDropdown?: string | null
41
+ helptextDropdownButton?: string | null
42
+ id?: string
43
+ includeSearch?: boolean
44
+ label?: string | null
45
+ maxlength?: number | null
46
+ minlength?: number | null
47
+ multiple?: boolean
48
+ name?: string
49
+ optionalTag?: boolean
50
+ optionalText?: string
51
+ options?: IPktComboboxOption[]
52
+ placeholder?: string | null
53
+ requiredTag?: boolean
54
+ requiredText?: string
55
+ searchPlaceholder?: string
56
+ tagPlacement?: TPktComboboxTagPlacement | null
57
+ value?: string | string[]
58
+ }
59
+
60
+ export type TPktComboboxTagPlacement = 'inside' | 'outside'
61
+
62
+ declare global {
63
+ interface HTMLElementTagNameMap {
64
+ 'pkt-combobox': PktCombobox & HTMLSelectElement
65
+ }
66
+ }
67
+
68
+ @customElement('pkt-combobox')
69
+ export class PktCombobox extends PktInputElement implements IPktCombobox {
70
+ private helptextSlot: Ref<HTMLElement> = createRef()
71
+
72
+ constructor() {
73
+ super()
74
+ this.optionsController = new PktOptionsSlotController(this)
75
+ this.slotController = new PktSlotController(this, this.helptextSlot)
76
+ this.slotController.skipOptions = true
77
+ }
78
+
79
+ // Props / Attributes
80
+ @property({ type: String, reflect: true }) value: string | string[] = ''
81
+ @property({ type: Array }) options: IPktComboboxOption[] = []
82
+ @property({ type: Boolean }) allowUserInput: boolean = false
83
+ @property({ type: Boolean }) typeahead: boolean = false
84
+ @property({ type: Boolean }) includeSearch: boolean = false
85
+ @property({ type: String }) searchPlaceholder: string = ''
86
+ @property({ type: Boolean }) multiple: boolean = false
87
+ @property({ type: Number }) maxlength: number | null = null
88
+ @property({ type: String }) displayValueAs: string = specs.props.displayValueAs.default
89
+ @property({ type: String }) tagPlacement: TPktComboboxTagPlacement | null = null
90
+
91
+ // State
92
+ @state() private _options: IPktComboboxOption[] = []
93
+ @state() private _isOptionsOpen = false
94
+ @state() private _value: string[] = []
95
+ @state() private _userInfoMessage: string = ''
96
+ @state() private _addValueText: string | null = null
97
+ @state() private _maxIsReached: boolean = false
98
+ @state() private _search: string = ''
99
+ @state() private _inputFocus: boolean = false
100
+ @state() private _editingSingleValue: boolean = false
101
+
102
+ // Refs
103
+ inputRef: Ref<HTMLInputElement> = createRef()
104
+ arrowRef: Ref<HTMLButtonElement> = createRef()
105
+ listboxRef: Ref<PktListbox> = createRef()
106
+ focusRef: Ref<HTMLElement> = createRef()
107
+ optionTagRef: Ref<HTMLElement> = createRef()
108
+
109
+ // Lifecycle methods
110
+ connectedCallback(): void {
111
+ super.connectedCallback()
112
+
113
+ document &&
114
+ document.body.addEventListener('click', (e: MouseEvent) => {
115
+ if (this._isOptionsOpen && !this.contains(e.target as Node)) {
116
+ this.handleFocusOut(e)
117
+ }
118
+ })
119
+
120
+ this._options = []
121
+ if (this.optionsController.nodes.length) {
122
+ const options: IPktComboboxOption[] = []
123
+ this.optionsController.nodes.forEach((node: Element) => {
124
+ if (!node.textContent && !node.getAttribute('value')) return null
125
+ const option: IPktComboboxOption = {
126
+ value: node.getAttribute('value') || node.textContent || '',
127
+ label: node.textContent || node.getAttribute('value') || '',
128
+ }
129
+ if (node.getAttribute('data-prefix')) {
130
+ option.prefix = node.getAttribute('data-prefix') || undefined
131
+ }
132
+ if (node.getAttribute('tagskincolor')) {
133
+ option.tagSkinColor = node.getAttribute('tagskincolor') as TTagSkin
134
+ }
135
+ if (node.getAttribute('description')) {
136
+ option.description = node.getAttribute('description') || undefined
137
+ }
138
+ option.fulltext = option.value + option.label + (option.prefix || '')
139
+ options.push(option)
140
+ })
141
+ if (options.length) {
142
+ this.options = [...options]
143
+ this._options = [...options]
144
+ }
145
+ }
146
+ }
147
+
148
+ updated(changedProperties: PropertyValues): void {
149
+ if (changedProperties.has('_value')) {
150
+ this.valueChanged(this._value, changedProperties.get('_value') as string[])
151
+ }
152
+ if (changedProperties.has('value')) {
153
+ this._value = Array.isArray(this.value) ? this.value : this.value ? this.value.split(',') : []
154
+ if (!this.multiple && this._value.length > 1) {
155
+ this._value = [this._value[0]]
156
+ }
157
+ this.isMaxItemsReached()
158
+ }
159
+ if (changedProperties.has('options') && this.options.length) {
160
+ this._options = this.options
161
+ this._options.forEach((option) => {
162
+ option.fulltext = option.value + option.label + (option.prefix || '')
163
+ option.selected = this._value.includes(option.value)
164
+ })
165
+ }
166
+ if (changedProperties.has('_search')) {
167
+ this.dispatchEvent(
168
+ new CustomEvent('search', {
169
+ detail: this._search,
170
+ bubbles: false,
171
+ }),
172
+ )
173
+ }
174
+ super.updated(changedProperties)
175
+ }
176
+
177
+ attributeChangedCallback(name: string, _old: string | null, value: string | null): void {
178
+ if (name === 'value') {
179
+ this._value = Array.isArray(this.value) ? this.value : this.value ? this.value.split(',') : []
180
+ if (!this.multiple && this._value.length > 1) {
181
+ this._value = [this._value[0]]
182
+ }
183
+ }
184
+ if (name === 'options') {
185
+ this._options = this.options
186
+ this._options.forEach((option) => {
187
+ option.fulltext = option.value + option.label + (option.prefix || '')
188
+ })
189
+ this._search = ''
190
+ }
191
+ super.attributeChangedCallback(name, _old, value)
192
+ }
193
+
194
+ // Render methods
195
+ render() {
196
+ return html`
197
+ <pkt-input-wrapper
198
+ .label=${this.label}
199
+ .helptext=${this.helptext}
200
+ .helptextDropdown=${ifDefined(this.helptextDropdown)}
201
+ .helptextDropdownButton=${ifDefined(this.helptextDropdownButton)}
202
+ ?fullwidth=${this.fullwidth}
203
+ ?hasError=${this.hasError}
204
+ ?inline=${this.inline}
205
+ ?disabled=${this.disabled}
206
+ .errorMessage=${this.errorMessage}
207
+ ?optionalTag=${this.optionalTag}
208
+ .optionalText=${this.optionalText}
209
+ ?requiredTag=${this.requiredTag}
210
+ .requiredText=${this.requiredText}
211
+ ?useWrapper=${this.useWrapper}
212
+ .forId=${this.allowUserInput || this.typeahead ? this.id + '-input' : this.id + '-arrow'}
213
+ class="pkt-combobox__wrapper"
214
+ @labelClick=${this.handleInputClick}
215
+ >
216
+ <div class="pkt-contents" ${ref(this.helptextSlot)} name="helptext" slot="helptext"></div>
217
+ <div class="pkt-combobox" @focusout=${this.handleFocusOut}>
218
+ <div
219
+ class=${classMap({
220
+ 'pkt-combobox__input': true,
221
+ 'pkt-combobox__input--fullwidth': this.fullwidth,
222
+ 'pkt-combobox__input--open': this._isOptionsOpen,
223
+ 'pkt-combobox__input--error': this.hasError,
224
+ 'pkt-combobox__input--disabled': this.disabled,
225
+ })}
226
+ tabindex="-1"
227
+ @click=${this.handleInputClick}
228
+ >
229
+ ${this.placeholder &&
230
+ (!this._value.length || (this.multiple && this.tagPlacement == 'outside')) &&
231
+ !this._inputFocus
232
+ ? html`<span class="pkt-combobox__placeholder">${this.placeholder}</span>`
233
+ : this.tagPlacement !== 'outside'
234
+ ? this.renderSingleOrMultipleValues()
235
+ : nothing}
236
+ ${this.renderInputField()}
237
+ <div
238
+ class="pkt-btn pkt-btn--tertiary pkt-combobox__arrow"
239
+ @click=${this.handleArrowClick}
240
+ @keydown=${this.handleArrowClick}
241
+ id="${this.id}-arrow"
242
+ ${ref(this.arrowRef)}
243
+ aria-expanded=${this._isOptionsOpen}
244
+ aria-controls="${this.id}-listbox"
245
+ aria-haspopup="listbox"
246
+ aria-label="Åpne liste"
247
+ ?disabled=${this.disabled}
248
+ ?data-disabled=${this.disabled}
249
+ role="button"
250
+ tabindex="${this.disabled ? '-1' : '0'}"
251
+ >
252
+ <pkt-icon
253
+ class=${classMap({
254
+ 'pkt-combobox__arrow-icon': true,
255
+ 'pkt-combobox__arrow-icon--open': this._isOptionsOpen,
256
+ })}
257
+ name="chevron-thin-down"
258
+ ></pkt-icon>
259
+ </div>
260
+ <div ${ref(this.focusRef)} tabindex="-1" @keydown=${this.handleArrowClick}></div>
261
+ </div>
262
+
263
+ <pkt-listbox
264
+ id="${this.id}-listbox"
265
+ .options=${this._options}
266
+ .isOpen=${this._isOptionsOpen}
267
+ .searchPlaceholder=${this.searchPlaceholder}
268
+ .label="Liste: ${this.label || ''}"
269
+ ?includeSearch=${this.includeSearch}
270
+ ?isMultiSelect=${this.multiple}
271
+ ?allowUserInput=${this.allowUserInput && !this._maxIsReached}
272
+ ?maxIsReached=${this._maxIsReached}
273
+ .customUserInput=${ifDefined(this._addValueText)}
274
+ .userMessage=${this._userInfoMessage}
275
+ @search=${this.handleSearch}
276
+ @option-toggle=${this.handleOptionToggled}
277
+ @select-all=${this.addAllOptions}
278
+ @close-options=${() => (this._isOptionsOpen = false)}
279
+ .searchValue=${this._search || null}
280
+ .maxLength=${this.maxlength || 0}
281
+ ${ref(this.listboxRef)}
282
+ ></pkt-listbox>
283
+ </div>
284
+
285
+ ${this.tagPlacement === 'outside' && this.multiple
286
+ ? html`<div class="pkt-combobox__tags-outside">
287
+ ${this.renderSingleOrMultipleValues()}
288
+ </div>`
289
+ : nothing}
290
+ </pkt-input-wrapper>
291
+ `
292
+ }
293
+
294
+ renderInputField() {
295
+ return this.typeahead || this.allowUserInput
296
+ ? html`
297
+ <div class="pkt-combobox__input-div combobox__input">
298
+ <input
299
+ type="text"
300
+ id="${this.id}-input"
301
+ name=${(this.name || this.id) + '-input'}
302
+ @input=${this.handleInput}
303
+ @keydown=${this.handleInputKeydown}
304
+ @focus=${this.handleFocus}
305
+ @blur=${this.handleBlur}
306
+ autocomplete="off"
307
+ role="combobox"
308
+ aria-label=${ifDefined(this.label)}
309
+ aria-autocomplete=${this.typeahead ? 'both' : 'list'}
310
+ aria-controls="${this.id}-listbox"
311
+ aria-multiselectable=${ifDefined(this.multiple ? 'true' : undefined)}
312
+ aria-activedescendant=${ifDefined(
313
+ this._value[0] && !!this.findValueInOptions(this._value[0])
314
+ ? `${this.id}-listbox-${this.findIndexInOptions(this._value[0])}`
315
+ : undefined,
316
+ )}
317
+ ${ref(this.inputRef)}
318
+ />
319
+ </div>
320
+ `
321
+ : html`
322
+ <input
323
+ type="hidden"
324
+ id="${this.id}-input"
325
+ name=${(this.name || this.id) + '-input'}
326
+ .value=${this._value.join(',')}
327
+ ${ref(this.inputRef)}
328
+ />
329
+ `
330
+ }
331
+
332
+ renderSingleOrMultipleValues() {
333
+ const isSingleValueDisplay = !this.multiple
334
+
335
+ // enkeltverdi som tekst
336
+ const singleValueContent = !this._editingSingleValue
337
+ ? this.renderValueTag(this.findValueInOptions(this._value[0]))
338
+ : null
339
+
340
+ // Multiple vises som tags
341
+ const multipleValuesContent = repeat(
342
+ this._value,
343
+ (value) => value,
344
+ (value) => {
345
+ const option = this.findValueInOptions(value)
346
+ const tagSkinColor = this.options.find((o) => o.value === value)?.tagSkinColor
347
+ return html`
348
+ <pkt-tag
349
+ skin=${tagSkinColor || 'blue-dark'}
350
+ ?closeTag=${!this.disabled}
351
+ @close=${() => this.handleTagRemove(value)}
352
+ >
353
+ ${this.renderValueTag(option)}
354
+ </pkt-tag>
355
+ `
356
+ },
357
+ )
358
+
359
+ return isSingleValueDisplay ? singleValueContent : multipleValuesContent
360
+ }
361
+
362
+ renderValueTag(option: IPktComboboxOption | null) {
363
+ if (!option) return ''
364
+ switch (this.displayValueAs) {
365
+ case 'prefixAndValue':
366
+ return html`<span data-focusfix=${this.id}>${option.prefix || ''} ${option.value}</span>`
367
+ case 'value':
368
+ return html`<span data-focusfix=${this.id}>${option.value}</span>`
369
+ case 'label':
370
+ default:
371
+ return html`<span data-focusfix=${this.id}>${option.label || option.value}</span>`
372
+ }
373
+ }
374
+
375
+ // Event handlers
376
+
377
+ handleInput(e: InputEvent): void {
378
+ e.stopPropagation()
379
+ e.stopImmediatePropagation()
380
+
381
+ if (this.disabled) return
382
+
383
+ this.touched = true
384
+ const input = e.target as HTMLInputElement
385
+ this._search = input.value
386
+ this.checkForMatches()
387
+
388
+ if (this.typeahead) {
389
+ if (this._search) {
390
+ this._options = this.options.filter((option) =>
391
+ option.fulltext?.toLowerCase().includes(this._search.toLowerCase()),
392
+ )
393
+
394
+ if (e.inputType !== 'deleteContentBackward') {
395
+ const matchingOptions = this._options.filter(
396
+ (option) =>
397
+ !option.selected &&
398
+ option.label?.toLowerCase().startsWith(this._search.toLowerCase()),
399
+ )
400
+
401
+ if (
402
+ matchingOptions.length > 0 &&
403
+ this.inputRef.value &&
404
+ this.inputRef.value.type !== 'hidden'
405
+ ) {
406
+ const match = matchingOptions[0]
407
+
408
+ if (match?.label) {
409
+ input.value = match.label
410
+ window.setTimeout(
411
+ () => input.setSelectionRange(this._search.length, input.value.length),
412
+ 0,
413
+ )
414
+ input.selectionDirection = 'backward'
415
+ }
416
+ }
417
+ }
418
+ } else {
419
+ this._options = [...this.options]
420
+ }
421
+ }
422
+ }
423
+
424
+ private handleFocus(): void {
425
+ if (this.disabled) return
426
+
427
+ if (
428
+ !this.multiple &&
429
+ this._value[0] &&
430
+ this.inputRef.value &&
431
+ this.inputRef.value.type !== 'hidden'
432
+ ) {
433
+ const option = this.findValueInOptions(this._value[0])
434
+ this._editingSingleValue = true
435
+ this.inputRef.value.value =
436
+ this.displayValueAs === 'label' && option?.label ? option.label : this._value[0]
437
+ }
438
+ this._inputFocus = true
439
+ this._search = ''
440
+ this._options = [...this.options]
441
+ this._isOptionsOpen = true
442
+ this.onFocus()
443
+
444
+ this.requestUpdate() // Ensure the UI updates
445
+ }
446
+
447
+ private handleFocusOut(e: FocusEvent): void {
448
+ if (this.disabled) return
449
+
450
+ // Triggered when focus completely leaves the combobox and its children
451
+ if (
452
+ (e.relatedTarget as Element)?.closest('pkt-combobox')?.id !== this.id &&
453
+ (e.relatedTarget as Element)?.closest('pkt-combobox')?.id !== this.id &&
454
+ (e.target as Element)?.getAttribute('data-focusfix') !== this.id &&
455
+ e.relatedTarget !== this.focusRef.value &&
456
+ e.relatedTarget !== this.inputRef.value &&
457
+ e.relatedTarget !== this.arrowRef.value &&
458
+ this._isOptionsOpen
459
+ ) {
460
+ this._inputFocus = false
461
+ this._addValueText = null
462
+ this._userInfoMessage = ''
463
+ this._search = ''
464
+
465
+ // If value in text input, check if it should be added
466
+ if (
467
+ this.inputRef.value &&
468
+ this.inputRef.value.type !== 'hidden' &&
469
+ this.inputRef.value.value !== ''
470
+ ) {
471
+ const val = this.inputRef.value.value
472
+ if (!this._value.includes(val) && !this._options.find((o) => o.label === val)) {
473
+ if (this.allowUserInput) {
474
+ this.addNewUserValue(val)
475
+ } else if (!this.multiple) {
476
+ this.removeValue(this._value[0])
477
+ }
478
+ } else if (!this._value.includes(val)) {
479
+ this.setSelected(val)
480
+ }
481
+ this.inputRef.value.value = ''
482
+ }
483
+ this._isOptionsOpen = false
484
+ this.onBlur()
485
+ }
486
+ }
487
+
488
+ private handleBlur(): void {
489
+ this._inputFocus = false
490
+ this._editingSingleValue = false
491
+ this.onBlur()
492
+ }
493
+
494
+ private handleInputClick(e: MouseEvent): void {
495
+ if (this.disabled) return
496
+
497
+ if (
498
+ e.currentTarget &&
499
+ e.currentTarget !== this.arrowRef.value &&
500
+ this.inputRef.value?.type !== 'hidden'
501
+ ) {
502
+ this.inputRef.value?.focus()
503
+ } else {
504
+ this.handleArrowClick(e)
505
+ }
506
+ }
507
+
508
+ private handleArrowClick(e: MouseEvent | KeyboardEvent): void {
509
+ if (this.disabled) return
510
+
511
+ if (e instanceof KeyboardEvent && e.key) {
512
+ if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'ArrowDown') return
513
+ }
514
+
515
+ e.stopImmediatePropagation()
516
+ e.preventDefault()
517
+
518
+ this._isOptionsOpen = !this._isOptionsOpen
519
+
520
+ if (this._isOptionsOpen) {
521
+ this.listboxRef.value?.focusFirstOrSelectedOption()
522
+ } else {
523
+ this.arrowRef.value?.focus()
524
+ }
525
+ }
526
+
527
+ private handleOptionToggled(e: CustomEvent) {
528
+ this.toggleValue(e.detail)
529
+ }
530
+
531
+ private handleSearch(e: CustomEvent) {
532
+ e.stopPropagation()
533
+ this._search = e.detail.toLowerCase()
534
+ }
535
+
536
+ private handleInputKeydown(e: KeyboardEvent): void {
537
+ switch (e.key) {
538
+ case ',':
539
+ case 'Enter':
540
+ e.preventDefault()
541
+ this.addValue()
542
+ break
543
+ case 'Backspace':
544
+ if (!this._search && this.inputRef.value?.type === 'hidden') this.removeLastValue(e)
545
+ break
546
+ case 'Tab':
547
+ case 'ArrowDown':
548
+ if (!e.shiftKey) {
549
+ this.listboxRef.value?.focusFirstOrSelectedOption()
550
+ e.preventDefault()
551
+ }
552
+ break
553
+ case 'Escape':
554
+ this._isOptionsOpen = false
555
+ this.arrowRef.value?.focus() // Return focus to the button
556
+ e.preventDefault()
557
+ break
558
+ default:
559
+ break
560
+ }
561
+ }
562
+
563
+ private handleTagRemove(value: string | null): void {
564
+ this.removeSelected(value)
565
+ }
566
+
567
+ private blurInput(): void {
568
+ if (this.inputRef.value && this.inputRef.value.matches(':focus')) {
569
+ this.inputRef.value.blur()
570
+ }
571
+ }
572
+
573
+ private checkForMatches() {
574
+ // sjekker om verdiene bruker skriver inn finnes, er valgt eller kan legges til
575
+ //setter riktig infomelding til bruker
576
+ const inputValue = this.inputRef.value?.value || this._search || ''
577
+ const searchValue = inputValue.trim().toLowerCase() || ''
578
+
579
+ if (!searchValue) {
580
+ if (!this.multiple && this._value[0]) {
581
+ this.removeValue(this._value[0])
582
+ }
583
+
584
+ this.resetComboboxInput(false)
585
+ return
586
+ }
587
+
588
+ const matchedValues = this._value.find((value) => value.toLowerCase() === searchValue)
589
+ const matchedOptions: IPktComboboxOption[] = this._options.filter(
590
+ (option) => option.label?.toLowerCase().includes(searchValue) ?? false,
591
+ )
592
+ const matchedOption = matchedOptions.find(
593
+ (option) =>
594
+ option.label?.toLowerCase() === searchValue || option.value.toLowerCase() === searchValue,
595
+ )
596
+
597
+ // sett riktig infomelding til bruker
598
+ switch (true) {
599
+ case (matchedOptions.length === 0 || !matchedOption) && this.allowUserInput:
600
+ this._addValueText = inputValue
601
+ this._userInfoMessage = ''
602
+ break
603
+
604
+ case matchedOptions.length === 0 && !this.allowUserInput:
605
+ this._addValueText = null
606
+ this._userInfoMessage = 'Ingen match i søket'
607
+ break
608
+
609
+ case !!matchedValues:
610
+ this._addValueText = null
611
+ this._userInfoMessage = 'Verdien er allerede valgt'
612
+ break
613
+
614
+ case matchedOptions.length > 1:
615
+ this._addValueText = null
616
+ this._userInfoMessage = ''
617
+ break
618
+
619
+ default:
620
+ this._addValueText = null
621
+ this._userInfoMessage = '' // Default for å fjerne melding
622
+ }
623
+ }
624
+
625
+ private findValueInOptions(value: string | null): IPktComboboxOption | null {
626
+ return (
627
+ this.options.find((option) => {
628
+ return option.value === value || option.label === value
629
+ }) || null
630
+ )
631
+ }
632
+
633
+ private findIndexInOptions(value: string | null): number {
634
+ return this._options.findIndex((option) => {
635
+ return option.value === value || option.label === value
636
+ })
637
+ }
638
+
639
+ private isMaxItemsReached(): boolean {
640
+ const isReached = this.maxlength !== null && this._value.length >= this.maxlength
641
+ if (!isReached) {
642
+ this._maxIsReached = false
643
+ } else {
644
+ this._maxIsReached = true
645
+ }
646
+ return isReached
647
+ }
648
+
649
+ public toggleValue(value: string | null): void {
650
+ if (this.disabled) return
651
+
652
+ this.touched = true
653
+ this._userInfoMessage = ''
654
+ this._addValueText = null
655
+
656
+ const valueFromOptions: string | null = this.findValueInOptions(value)?.value || null
657
+ const isSelected: boolean = this._value.includes(value || valueFromOptions || '')
658
+ const isInOption: boolean = !!valueFromOptions
659
+ const isDisabled: boolean = this._options.find((o) => o.value === value)?.disabled || false
660
+ const isEmpty: boolean = !value?.trim()
661
+ const isSingle: boolean = !this.multiple
662
+ const isMultiple: boolean = this.multiple
663
+ const isMaxItemsReached: boolean = this.isMaxItemsReached()
664
+
665
+ let shouldOptionsBeOpen: boolean = false
666
+ let shouldResetInput: boolean = true
667
+ let userInfoMessage: string | null = ''
668
+ let searchValue: string | null = ''
669
+
670
+ if (isDisabled) return
671
+
672
+ // Dersom ikke i listen og allowUserInput er true
673
+ if (!isInOption && this.allowUserInput && !isEmpty) {
674
+ this.addNewUserValue(value)
675
+ userInfoMessage = 'Ny verdi lagt til'
676
+ shouldOptionsBeOpen = !isMultiple
677
+ }
678
+
679
+ // Dersom ikke i listen men allowUserInput er false
680
+ else if (!isInOption && !this.allowUserInput) {
681
+ if (isSingle && this._value[0]) {
682
+ this.removeValue(this._value[0])
683
+ }
684
+ shouldResetInput = false
685
+ shouldOptionsBeOpen = true
686
+ userInfoMessage = 'Ingen treff i søket'
687
+ }
688
+
689
+ // Dersom verdien er valgt allerede
690
+ else if (isSelected) {
691
+ this.removeValue(valueFromOptions)
692
+ shouldOptionsBeOpen = true
693
+ }
694
+
695
+ // Dersom verdien er en tom streng, og det er enkeltvalg
696
+ else if (isEmpty && isSingle) {
697
+ this.removeAllSelected()
698
+ shouldOptionsBeOpen = true
699
+ }
700
+
701
+ // Dersom det er enkeltvalg
702
+ else if (isSingle) {
703
+ this._value[0] && this.removeSelected(this._value[0])
704
+ this.setSelected(valueFromOptions)
705
+ shouldOptionsBeOpen = false
706
+ if (this.inputRef.value && this.inputRef.value.type !== 'hidden') {
707
+ this.inputRef.value.value = ''
708
+ this.inputRef.value.blur()
709
+ }
710
+ }
711
+
712
+ // Dersom det er flervalg og mulig å legge til fler
713
+ else if (isMultiple && !isMaxItemsReached) {
714
+ this.setSelected(valueFromOptions)
715
+ shouldOptionsBeOpen = true
716
+ }
717
+
718
+ // Dersom det er flervalg og maks antall er nådd
719
+ else if (isMultiple && isMaxItemsReached) {
720
+ this._userInfoMessage = 'Maks antall valg nådd'
721
+ shouldResetInput = false
722
+ searchValue = value
723
+ }
724
+
725
+ // Dersom ingen av de over passer
726
+ else {
727
+ isSingle && this.removeAllSelected()
728
+ this._userInfoMessage = 'Ingen gyldig verdi valgt'
729
+ shouldResetInput = false
730
+ shouldOptionsBeOpen = true
731
+ searchValue = value
732
+ }
733
+
734
+ this._isOptionsOpen = shouldOptionsBeOpen
735
+ if (!shouldOptionsBeOpen) {
736
+ window.setTimeout(() => {
737
+ this.focusRef.value?.focus()
738
+ }, 0)
739
+ }
740
+ this._userInfoMessage = userInfoMessage
741
+ this._search = searchValue || ''
742
+ this.resetComboboxInput(shouldResetInput)
743
+ isMultiple && this.isMaxItemsReached()
744
+ }
745
+
746
+ private setSelected(value: string | null): void {
747
+ if (this._value.includes(value as string)) return
748
+
749
+ if (this.multiple && this.isMaxItemsReached()) {
750
+ this._userInfoMessage = 'Maks antall valg nådd'
751
+ return
752
+ }
753
+
754
+ !this.multiple && this.removeAllSelected()
755
+
756
+ this._value = value ? [...this._value, value] : this._value
757
+ this._options = this._options.map((option) => {
758
+ if (option.value === value) {
759
+ option.selected = true
760
+ }
761
+ return option
762
+ })
763
+
764
+ this.resetComboboxInput(true)
765
+ }
766
+
767
+ private removeSelected(value: string | null): void {
768
+ if (!value) return
769
+ this._value = this._value.filter((v) => v !== value)
770
+ const _opt = this.findValueInOptions(value)
771
+ if (_opt) {
772
+ _opt.selected = false
773
+ if (_opt.userAdded) {
774
+ this._options = [...this._options.filter((o) => o.value !== value)]
775
+ this.options = [...this.options.filter((o) => o.value !== value)]
776
+ } else {
777
+ this._options = [...this._options, _opt]
778
+ }
779
+ } else if (!value && !this.multiple) {
780
+ this._options = this._options.map((option) => {
781
+ option.selected = false
782
+ return option
783
+ })
784
+ }
785
+ }
786
+
787
+ private addAllOptions(): void {
788
+ if (!this.multiple) return
789
+ if (this.maxlength && this._options.length > this.maxlength) {
790
+ this._userInfoMessage = 'For mange valgt'
791
+ return
792
+ }
793
+
794
+ this._value = this._options.map((option) => option.value)
795
+ this._options = this._options.map((option) => {
796
+ option.selected = true
797
+ return option
798
+ })
799
+ this.requestUpdate()
800
+ }
801
+
802
+ private removeAllSelected(): void {
803
+ this._value = []
804
+ this._options = this._options.map((option) => {
805
+ option.selected = false
806
+ return option
807
+ })
808
+ this._options = this._options.filter((option) => !option.userAdded)
809
+ this.requestUpdate()
810
+ }
811
+
812
+ private addValue(): void {
813
+ const input = this.inputRef.value?.value.trim() || ''
814
+ this._search = input
815
+ this.toggleValue(input)
816
+ }
817
+
818
+ private removeValue(value: string | null): void {
819
+ this._value = this.multiple ? this._value.filter((v) => v !== value) : []
820
+ this.removeSelected(value)
821
+ }
822
+
823
+ private addNewUserValue(value: string | null): void {
824
+ if (!value || value.trim() === '') return
825
+
826
+ if (!this.multiple) {
827
+ this._value[0] && this.removeSelected(this._value[0])
828
+ this._value = [value]
829
+ this._isOptionsOpen = false
830
+ this.blurInput()
831
+ } else if (!this.findValueInOptions(value)) {
832
+ if (this.isMaxItemsReached()) return
833
+ this._value = [...this._value, value]
834
+ }
835
+
836
+ const newOption: IPktComboboxOption = { value, label: value, userAdded: true }
837
+
838
+ this.options = [newOption, ...this.options]
839
+ this._options = [newOption, ...this._options]
840
+ this.setSelected(value)
841
+ this.requestUpdate()
842
+ }
843
+
844
+ private resetComboboxInput(shouldResetInput: boolean = true): void {
845
+ this._addValueText = null
846
+ if (this.inputRef.value && this.inputRef.value.type !== 'hidden' && shouldResetInput) {
847
+ this._search = ''
848
+ if (this.multiple) {
849
+ this.inputRef.value.value = ''
850
+ } else {
851
+ const option = this.findValueInOptions(this._value[0])
852
+ window.setTimeout(() => {
853
+ if (!this.inputRef.value || this.inputRef.value.type === 'hidden') return
854
+ this.inputRef.value.value =
855
+ this.displayValueAs === 'label' && option?.label ? option.label : this._value[0] || ''
856
+ }, 0)
857
+ this._userInfoMessage = ''
858
+ }
859
+ }
860
+ this._options = [...this.options]
861
+ }
862
+
863
+ private removeLastValue(e: Event): void {
864
+ if (this._value.length === 0) return
865
+
866
+ e.preventDefault()
867
+
868
+ const val = this._value[this._value.length - 1]
869
+ val && this.removeSelected(val)
870
+
871
+ this.isMaxItemsReached()
872
+ }
873
+ }