@oslokommune/punkt-elements 15.4.5 → 16.0.2
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 +34 -0
- package/dist/{card-CnPjrdre.js → card-CmfUyl_s.js} +1 -1
- package/dist/{card-5S2r9UD1.cjs → card-Db9QSEqh.cjs} +1 -1
- package/dist/{checkbox-D98_NjcU.cjs → checkbox-Cpyay9_l.cjs} +1 -1
- package/dist/{checkbox-BSz71IeT.js → checkbox-D6nltMuc.js} +1 -1
- package/dist/combobox-Bv37b6cI.cjs +135 -0
- package/dist/combobox-CoO8T-F-.js +818 -0
- package/dist/{datepicker-SEKblnRR.cjs → datepicker-CrvQ5Y5w.cjs} +1 -1
- package/dist/{datepicker-nnyTW0vf.js → datepicker-DbsIuC5Z.js} +2 -2
- package/dist/index.d.ts +157 -90
- package/dist/{input-element-Bkv6Yxld.js → input-element-BGNbdzy2.js} +1 -1
- package/dist/{input-element-DM0tY799.cjs → input-element-CSDVA3Y6.cjs} +1 -1
- package/dist/listbox-Dm2mKp6_.cjs +101 -0
- package/dist/listbox-OdkIn9_A.js +431 -0
- package/dist/pkt-card.cjs +1 -1
- package/dist/pkt-card.js +1 -1
- package/dist/pkt-checkbox.cjs +1 -1
- package/dist/pkt-checkbox.js +1 -1
- package/dist/pkt-combobox.cjs +1 -1
- package/dist/pkt-combobox.js +1 -1
- package/dist/pkt-datepicker.cjs +1 -1
- package/dist/pkt-datepicker.js +2 -2
- package/dist/pkt-header.cjs +1 -1
- package/dist/pkt-header.js +1 -1
- package/dist/pkt-index.cjs +1 -1
- package/dist/pkt-index.js +9 -9
- package/dist/pkt-listbox.cjs +1 -1
- package/dist/pkt-listbox.js +1 -1
- package/dist/pkt-options-controller-BogGk-6J.cjs +1 -0
- package/dist/{pkt-options-controller-BcGywCmf.js → pkt-options-controller-Z-bPox7n.js} +2 -2
- package/dist/pkt-radiobutton.cjs +1 -1
- package/dist/pkt-radiobutton.js +1 -1
- package/dist/pkt-select.cjs +1 -1
- package/dist/pkt-select.js +1 -1
- package/dist/pkt-tag.cjs +1 -1
- package/dist/pkt-tag.js +1 -1
- package/dist/pkt-textarea.cjs +1 -1
- package/dist/pkt-textarea.js +1 -1
- package/dist/pkt-textinput.cjs +1 -1
- package/dist/pkt-textinput.js +1 -1
- package/dist/{radiobutton-95wp024h.cjs → radiobutton-CNHCpKn0.cjs} +1 -1
- package/dist/{radiobutton-CTFAV5GU.js → radiobutton-DgC27mb0.js} +1 -1
- package/dist/{select-YLvYAQX6.js → select-7VuYtPZv.js} +2 -2
- package/dist/{select-CZ_Lx5W6.cjs → select-PWPy5gTB.cjs} +1 -1
- package/dist/{tag-68q0_Sn0.js → tag-DZPqFiem.js} +37 -33
- package/dist/tag-DmbgBCKu.cjs +27 -0
- package/dist/{textarea-CuTsE1WX.cjs → textarea-CO7Ikug5.cjs} +1 -1
- package/dist/{textarea-DhWH99qN.js → textarea-VpCEjVFx.js} +1 -1
- package/dist/{textinput-BCi9p0Du.js → textinput-C2AZ9ss2.js} +1 -1
- package/dist/{textinput-st4Vml5J.cjs → textinput-DRFZU3dA.cjs} +1 -1
- package/package.json +4 -4
- package/src/components/card/card.ts +1 -0
- package/src/components/combobox/combobox-base.ts +158 -0
- package/src/components/combobox/combobox-handlers.ts +419 -0
- package/src/components/combobox/combobox-types.ts +10 -0
- package/src/components/combobox/combobox-utils.ts +135 -0
- package/src/components/combobox/combobox-value.ts +248 -0
- package/src/components/combobox/combobox.accessibility.test.ts +243 -0
- package/src/components/combobox/{combobox.test.ts → combobox.core.test.ts} +104 -46
- package/src/components/combobox/combobox.interaction.test.ts +436 -0
- package/src/components/combobox/combobox.selection.test.ts +543 -0
- package/src/components/combobox/combobox.ts +260 -734
- package/src/components/listbox/index.ts +2 -0
- package/src/components/listbox/listbox.interaction.test.ts +580 -0
- package/src/components/listbox/listbox.test.ts +32 -6
- package/src/components/listbox/listbox.ts +109 -126
- package/src/components/tag/tag.ts +3 -0
- package/dist/combobox-C5YcNVSZ.cjs +0 -128
- package/dist/combobox-cer7PLSE.js +0 -533
- package/dist/listbox-C7NEa9SU.cjs +0 -96
- package/dist/listbox-Cykec1bj.js +0 -361
- package/dist/pkt-options-controller-BnTmkl3g.cjs +0 -1
- package/dist/tag-BnT5onW2.cjs +0 -26
|
@@ -1,32 +1,27 @@
|
|
|
1
1
|
import { html, nothing, PropertyValues } from 'lit'
|
|
2
2
|
import { ifDefined } from 'lit/directives/if-defined.js'
|
|
3
|
-
import { customElement
|
|
4
|
-
import {
|
|
3
|
+
import { customElement } from 'lit/decorators.js'
|
|
4
|
+
import { ref } from 'lit/directives/ref.js'
|
|
5
5
|
import { classMap } from 'lit/directives/class-map.js'
|
|
6
6
|
import { repeat } from 'lit/directives/repeat.js'
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
IPktComboboxOption,
|
|
10
|
+
TPktComboboxTagPlacement,
|
|
11
|
+
TPktComboboxDisplayValue,
|
|
12
|
+
} from './combobox-types'
|
|
13
|
+
import { findOptionByValue, findOptionIndex } from 'shared-utils/combobox/option-utils'
|
|
14
|
+
import { getSingleValueForInput } from 'shared-utils/combobox/input-utils'
|
|
15
|
+
import { slotUtils, optionStateUtils } from './combobox-utils'
|
|
16
|
+
import { ComboboxHandlers } from './combobox-handlers'
|
|
11
17
|
|
|
12
18
|
import '../input-wrapper'
|
|
13
19
|
import '../icon'
|
|
14
20
|
import '../tag'
|
|
15
21
|
import '../listbox'
|
|
16
|
-
import PktListbox from '../listbox'
|
|
17
|
-
import { TTagSkin } from '../tag'
|
|
18
22
|
|
|
19
|
-
export
|
|
20
|
-
|
|
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
|
-
}
|
|
23
|
+
// Re-export types for backward compatibility
|
|
24
|
+
export type { IPktComboboxOption, TPktComboboxTagPlacement } from './combobox-types'
|
|
30
25
|
|
|
31
26
|
export interface IPktCombobox {
|
|
32
27
|
allowUserInput?: boolean
|
|
@@ -57,10 +52,9 @@ export interface IPktCombobox {
|
|
|
57
52
|
tagPlacement?: TPktComboboxTagPlacement | null
|
|
58
53
|
tagText?: string | null
|
|
59
54
|
value?: string | string[]
|
|
55
|
+
isOpen?: boolean
|
|
60
56
|
}
|
|
61
57
|
|
|
62
|
-
export type TPktComboboxTagPlacement = 'inside' | 'outside'
|
|
63
|
-
|
|
64
58
|
declare global {
|
|
65
59
|
interface HTMLElementTagNameMap {
|
|
66
60
|
'pkt-combobox': PktCombobox & HTMLSelectElement
|
|
@@ -68,57 +62,20 @@ declare global {
|
|
|
68
62
|
}
|
|
69
63
|
|
|
70
64
|
@customElement('pkt-combobox')
|
|
71
|
-
export class PktCombobox extends
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
this.slotController = new PktSlotController(this, this.helptextSlot)
|
|
78
|
-
this.slotController.skipOptions = true
|
|
65
|
+
export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
|
|
66
|
+
// Bound handler for body click — stored for cleanup in disconnectedCallback
|
|
67
|
+
private handleBodyClick = (e: MouseEvent) => {
|
|
68
|
+
if (this._isOptionsOpen && !this.contains(e.target as Node)) {
|
|
69
|
+
this.closeAndProcessInput()
|
|
70
|
+
}
|
|
79
71
|
}
|
|
80
72
|
|
|
81
|
-
// Props / Attributes
|
|
82
|
-
@property({ type: String, reflect: true }) value: string | string[] = ''
|
|
83
|
-
@property({ type: Array }) options: IPktComboboxOption[] = []
|
|
84
|
-
@property({ type: Array }) defaultOptions: IPktComboboxOption[] = []
|
|
85
|
-
@property({ type: Boolean }) allowUserInput: boolean = false
|
|
86
|
-
@property({ type: Boolean }) typeahead: boolean = false
|
|
87
|
-
@property({ type: Boolean }) includeSearch: boolean = false
|
|
88
|
-
@property({ type: String }) searchPlaceholder: string = ''
|
|
89
|
-
@property({ type: Boolean }) multiple: boolean = false
|
|
90
|
-
@property({ type: Number }) maxlength: number | null = null
|
|
91
|
-
@property({ type: String }) displayValueAs: string = specs.props.displayValueAs.default
|
|
92
|
-
@property({ type: String }) tagPlacement: TPktComboboxTagPlacement | null = null
|
|
93
|
-
|
|
94
|
-
// State
|
|
95
|
-
@state() _options: IPktComboboxOption[] = []
|
|
96
|
-
@state() _value: string[] = [] // Internal value representation
|
|
97
|
-
@state() private _isOptionsOpen = false
|
|
98
|
-
@state() private _userInfoMessage: string = ''
|
|
99
|
-
@state() private _addValueText: string | null = null
|
|
100
|
-
@state() private _maxIsReached: boolean = false
|
|
101
|
-
@state() private _search: string = ''
|
|
102
|
-
@state() private _inputFocus: boolean = false
|
|
103
|
-
@state() private _editingSingleValue: boolean = false
|
|
104
|
-
|
|
105
|
-
// Refs
|
|
106
|
-
inputRef: Ref<HTMLInputElement> = createRef()
|
|
107
|
-
arrowRef: Ref<HTMLButtonElement> = createRef()
|
|
108
|
-
listboxRef: Ref<PktListbox> = createRef()
|
|
109
|
-
focusRef: Ref<HTMLElement> = createRef()
|
|
110
|
-
optionTagRef: Ref<HTMLElement> = createRef()
|
|
111
|
-
|
|
112
73
|
// Lifecycle methods
|
|
74
|
+
|
|
113
75
|
connectedCallback(): void {
|
|
114
76
|
super.connectedCallback()
|
|
115
77
|
|
|
116
|
-
document
|
|
117
|
-
document.body.addEventListener('click', (e: MouseEvent) => {
|
|
118
|
-
if (this._isOptionsOpen && !this.contains(e.target as Node)) {
|
|
119
|
-
this.handleFocusOut(e)
|
|
120
|
-
}
|
|
121
|
-
})
|
|
78
|
+
document?.body.addEventListener('click', this.handleBodyClick)
|
|
122
79
|
|
|
123
80
|
this._options = []
|
|
124
81
|
|
|
@@ -131,50 +88,77 @@ export class PktCombobox extends PktInputElement implements IPktCombobox {
|
|
|
131
88
|
|
|
132
89
|
// If options are provided via the options slot, we need to extract them
|
|
133
90
|
if (this.optionsController?.nodes && this.optionsController.nodes.length) {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
if (node.getAttribute('data-prefix')) {
|
|
142
|
-
option.prefix = node.getAttribute('data-prefix') || undefined
|
|
143
|
-
}
|
|
144
|
-
if (node.getAttribute('tagskincolor')) {
|
|
145
|
-
option.tagSkinColor = node.getAttribute('tagskincolor') as TTagSkin
|
|
146
|
-
}
|
|
147
|
-
if (node.getAttribute('description')) {
|
|
148
|
-
option.description = node.getAttribute('description') || undefined
|
|
149
|
-
}
|
|
150
|
-
option.fulltext = option.value + option.label + (option.prefix || '')
|
|
151
|
-
options.push(option)
|
|
152
|
-
})
|
|
153
|
-
if (options.length) {
|
|
154
|
-
this.options = [...options]
|
|
155
|
-
this._options = [...options]
|
|
91
|
+
const parsedOptions = slotUtils.parseOptionsFromSlot(this.optionsController.nodes)
|
|
92
|
+
if (parsedOptions.length) {
|
|
93
|
+
this.options = [...parsedOptions]
|
|
94
|
+
this._options = [...parsedOptions]
|
|
95
|
+
this._optionsFromSlot = true
|
|
96
|
+
this._lastSlotGeneration = this.optionsController.generation
|
|
156
97
|
}
|
|
157
98
|
}
|
|
158
99
|
}
|
|
159
100
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
101
|
+
protected willUpdate(changedProperties: Map<PropertyKey, unknown>): void {
|
|
102
|
+
// Re-parse slot options when the controller detects mutations.
|
|
103
|
+
// The controller increments its generation counter on each mutation, but
|
|
104
|
+
// doesn't set any reactive properties — so we detect the change here.
|
|
105
|
+
if (this._optionsFromSlot && this.optionsController) {
|
|
106
|
+
const currentGen = this.optionsController.generation
|
|
107
|
+
if (currentGen !== this._lastSlotGeneration) {
|
|
108
|
+
this._lastSlotGeneration = currentGen
|
|
109
|
+
const parsedOptions = slotUtils.parseOptionsFromSlot(this.optionsController.nodes)
|
|
110
|
+
const userAdded = this._options.filter((o) => o.userAdded)
|
|
111
|
+
this.options = [...userAdded, ...parsedOptions]
|
|
112
|
+
}
|
|
163
113
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
114
|
+
super.willUpdate(changedProperties)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
disconnectedCallback(): void {
|
|
118
|
+
super.disconnectedCallback()
|
|
119
|
+
document?.body.removeEventListener('click', this.handleBodyClick)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
firstUpdated(changedProperties: PropertyValues): void {
|
|
123
|
+
// Apply defaultValue before the base class firstUpdated, which calls
|
|
124
|
+
// valueChanged(defaultValue) — a no-op in combobox. Setting this.value
|
|
125
|
+
// here lets updated() handle the sync via the normal value-change path.
|
|
126
|
+
if (this.defaultValue !== null && !this.value) {
|
|
127
|
+
this.value = this.defaultValue
|
|
128
|
+
}
|
|
129
|
+
super.firstUpdated(changedProperties)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
updated(changedProperties: PropertyValues): void {
|
|
133
|
+
if (changedProperties.has('isOpen')) {
|
|
134
|
+
this._isOptionsOpen = this.isOpen
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Handle value and _value changes.
|
|
138
|
+
// Three cases:
|
|
139
|
+
// 1. value changed from our own syncValueAndDispatch (internal sync) — skip value handler,
|
|
140
|
+
// but still process concurrent _value changes
|
|
141
|
+
// 2. value changed externally — sync _value from value, dispatch events
|
|
142
|
+
// 3. Only _value changed — sync value from _value, dispatch events
|
|
143
|
+
const valueChanged = changedProperties.has('value')
|
|
144
|
+
const internalChanged = changedProperties.has('_value')
|
|
145
|
+
const isInternalSync = valueChanged && this._internalValueSync
|
|
146
|
+
|
|
147
|
+
if (isInternalSync) {
|
|
148
|
+
this._internalValueSync = false
|
|
149
|
+
if (internalChanged) {
|
|
150
|
+
this.syncValueAndDispatch(changedProperties.get('_value') as string[])
|
|
173
151
|
}
|
|
174
|
-
|
|
175
|
-
|
|
152
|
+
} else if (valueChanged) {
|
|
153
|
+
const oldInternal = [...this._value]
|
|
154
|
+
const newInternal = this.parseValue()
|
|
155
|
+
if (newInternal.join(',') !== this._value.join(',')) {
|
|
156
|
+
this._value = newInternal
|
|
176
157
|
}
|
|
177
|
-
this.
|
|
158
|
+
this.updateMaxReached()
|
|
159
|
+
this.syncValueAndDispatch(oldInternal)
|
|
160
|
+
} else if (internalChanged) {
|
|
161
|
+
this.syncValueAndDispatch(changedProperties.get('_value') as string[])
|
|
178
162
|
}
|
|
179
163
|
|
|
180
164
|
// If defaultOptions changed, update options (preserving userAdded)
|
|
@@ -186,35 +170,22 @@ export class PktCombobox extends PktInputElement implements IPktCombobox {
|
|
|
186
170
|
}
|
|
187
171
|
|
|
188
172
|
if (changedProperties.has('options')) {
|
|
189
|
-
// If options change, we need to update _options, but we need to preserve userAdded values
|
|
190
|
-
// Bug fix 2025-09-19: Fetch userAdded from _old_ options to prevent accidental overwrite
|
|
191
173
|
const prevOptions =
|
|
192
174
|
(changedProperties.get('options') as IPktComboboxOption[]) || this._options || []
|
|
193
|
-
const
|
|
194
|
-
const filteredUserAdded = userAddedValues.filter(
|
|
195
|
-
(userOpt) =>
|
|
196
|
-
!(Array.isArray(this.options) ? this.options : []).some(
|
|
197
|
-
(opt) => opt.value === userOpt.value,
|
|
198
|
-
),
|
|
199
|
-
)
|
|
200
|
-
const mergedOptions = [...filteredUserAdded, ...this.options]
|
|
175
|
+
const mergedOptions = optionStateUtils.mergeWithUserAdded(this.options, prevOptions)
|
|
201
176
|
this._options = mergedOptions
|
|
202
|
-
if (
|
|
177
|
+
if (mergedOptions.length > this.options.length) {
|
|
203
178
|
this.options = mergedOptions
|
|
204
179
|
}
|
|
205
180
|
|
|
206
|
-
this._options.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
option.fulltext = option.value + option.label + (option.prefix || '')
|
|
216
|
-
option.selected = option.selected || this._value.includes(option.value)
|
|
217
|
-
})
|
|
181
|
+
const syncResult = optionStateUtils.syncOptionsWithValues(this._options, this._value)
|
|
182
|
+
this._options = syncResult.options
|
|
183
|
+
|
|
184
|
+
if (syncResult.newValues.length > this._value.length) {
|
|
185
|
+
const oldValue = [...this._value]
|
|
186
|
+
this._value = syncResult.newValues
|
|
187
|
+
this.syncValueAndDispatch(oldValue)
|
|
188
|
+
}
|
|
218
189
|
}
|
|
219
190
|
if (changedProperties.has('_search')) {
|
|
220
191
|
this.dispatchEvent(
|
|
@@ -224,41 +195,83 @@ export class PktCombobox extends PktInputElement implements IPktCombobox {
|
|
|
224
195
|
}),
|
|
225
196
|
)
|
|
226
197
|
}
|
|
198
|
+
// Sync text input display value for single+typeahead when dropdown is closed
|
|
199
|
+
if (
|
|
200
|
+
!this._isOptionsOpen &&
|
|
201
|
+
!this.multiple &&
|
|
202
|
+
this._hasTextInput &&
|
|
203
|
+
this.inputRef.value &&
|
|
204
|
+
this.inputRef.value.type !== 'hidden'
|
|
205
|
+
) {
|
|
206
|
+
const displayValue = this._value[0]
|
|
207
|
+
? getSingleValueForInput(this._value[0], this.options, this.displayValueAs)
|
|
208
|
+
: ''
|
|
209
|
+
if (this.inputRef.value.value !== displayValue) {
|
|
210
|
+
this.inputRef.value.value = displayValue
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
227
214
|
super.updated(changedProperties)
|
|
228
215
|
}
|
|
229
216
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
217
|
+
/**
|
|
218
|
+
* Override form reset to properly restore combobox state.
|
|
219
|
+
* The base class deselects all options and sets value/defaultValue, but
|
|
220
|
+
* combobox needs to re-sync _options with the restored values and clean up
|
|
221
|
+
* user-added options and UI state.
|
|
222
|
+
*/
|
|
223
|
+
protected override formResetCallback(): void {
|
|
224
|
+
this.touched = false
|
|
225
|
+
|
|
226
|
+
// Restore value from defaultValue (set by base class firstUpdated from
|
|
227
|
+
// the initial value attribute, per MDN HTMLInputElement.defaultValue)
|
|
228
|
+
const resetValue = this.defaultValue || (this.multiple ? '' : '')
|
|
229
|
+
this.value = resetValue
|
|
230
|
+
this._value = this.parseValue()
|
|
231
|
+
|
|
232
|
+
// Remove user-added options, then re-sync selection state with restored _value.
|
|
233
|
+
// We must create new arrays because the base class mutates option objects in place.
|
|
234
|
+
this._options = this._options
|
|
235
|
+
.filter((o) => !o.userAdded)
|
|
236
|
+
.map((o) => ({ ...o, selected: this._value.includes(o.value) }))
|
|
237
|
+
this.options = this.options
|
|
238
|
+
.filter((o) => !o.userAdded)
|
|
239
|
+
.map((o) => ({ ...o, selected: this._value.includes(o.value) }))
|
|
240
|
+
|
|
241
|
+
// Reset UI state
|
|
242
|
+
this._search = ''
|
|
243
|
+
this._isOptionsOpen = false
|
|
244
|
+
this._userInfoMessage = ''
|
|
245
|
+
this._addValueText = null
|
|
246
|
+
this._inputFocus = false
|
|
247
|
+
this.updateMaxReached()
|
|
248
|
+
|
|
249
|
+
if (this.inputRef.value && this.inputRef.value.type !== 'hidden') {
|
|
250
|
+
this.inputRef.value.value = ''
|
|
244
251
|
}
|
|
252
|
+
|
|
253
|
+
this.internals.setFormValue('')
|
|
254
|
+
this.internals.ariaInvalid = 'false'
|
|
255
|
+
this.requestUpdate()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
attributeChangedCallback(name: string, _old: string | null, value: string | null): void {
|
|
259
|
+
// Don't set _value here for 'value' changes — this.value hasn't been updated yet
|
|
260
|
+
// (super.attributeChangedCallback does that). Let updated() handle the sync.
|
|
245
261
|
if (name === 'options') {
|
|
246
262
|
this._options = Array.isArray(this.options) ? [...this.options] : []
|
|
247
|
-
this._options.
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
this._value = [...this._value, option.value]
|
|
253
|
-
}
|
|
254
|
-
option.fulltext = option.value + option.label + (option.prefix || '')
|
|
255
|
-
})
|
|
263
|
+
const syncResult = optionStateUtils.syncOptionsWithValues(this._options, this._value)
|
|
264
|
+
this._options = syncResult.options
|
|
265
|
+
if (syncResult.newValues.length > this._value.length) {
|
|
266
|
+
this._value = syncResult.newValues
|
|
267
|
+
}
|
|
256
268
|
this._search = ''
|
|
257
269
|
}
|
|
258
270
|
super.attributeChangedCallback(name, _old, value)
|
|
259
271
|
}
|
|
260
272
|
|
|
261
273
|
// Render methods
|
|
274
|
+
|
|
262
275
|
render() {
|
|
263
276
|
return html`
|
|
264
277
|
<pkt-input-wrapper
|
|
@@ -277,7 +290,8 @@ export class PktCombobox extends PktInputElement implements IPktCombobox {
|
|
|
277
290
|
.requiredText=${this.requiredText}
|
|
278
291
|
.tagText=${this.tagText}
|
|
279
292
|
useWrapper=${this.useWrapper}
|
|
280
|
-
.forId=${this.
|
|
293
|
+
.forId=${this._hasTextInput ? this.id + '-input' : this.id + '-combobox'}
|
|
294
|
+
?hasFieldset=${!this._hasTextInput}
|
|
281
295
|
class="pkt-combobox__wrapper"
|
|
282
296
|
@labelClick=${this.handleInputClick}
|
|
283
297
|
>
|
|
@@ -291,10 +305,31 @@ export class PktCombobox extends PktInputElement implements IPktCombobox {
|
|
|
291
305
|
'pkt-combobox__input--error': this.hasError,
|
|
292
306
|
'pkt-combobox__input--disabled': this.disabled,
|
|
293
307
|
})}
|
|
294
|
-
|
|
308
|
+
id=${ifDefined(!this._hasTextInput ? `${this.id}-combobox` : undefined)}
|
|
309
|
+
role=${ifDefined(!this._hasTextInput ? 'combobox' : undefined)}
|
|
310
|
+
aria-expanded=${ifDefined(
|
|
311
|
+
!this._hasTextInput ? (this._isOptionsOpen ? 'true' : 'false') : undefined,
|
|
312
|
+
)}
|
|
313
|
+
aria-controls=${ifDefined(!this._hasTextInput ? `${this.id}-listbox` : undefined)}
|
|
314
|
+
aria-haspopup=${ifDefined(!this._hasTextInput ? 'listbox' : undefined)}
|
|
315
|
+
aria-labelledby=${ifDefined(
|
|
316
|
+
!this._hasTextInput ? `${this.id}-combobox-label` : undefined,
|
|
317
|
+
)}
|
|
318
|
+
aria-activedescendant=${ifDefined(
|
|
319
|
+
!this._hasTextInput &&
|
|
320
|
+
this._value[0] &&
|
|
321
|
+
!!findOptionByValue(this.options, this._value[0])
|
|
322
|
+
? `${this.id}-listbox-${findOptionIndex(this._options, this._value[0])}`
|
|
323
|
+
: undefined,
|
|
324
|
+
)}
|
|
325
|
+
aria-description=${ifDefined(this._selectionDescription || undefined)}
|
|
326
|
+
tabindex=${!this._hasTextInput ? (this.disabled ? '-1' : '0') : '-1'}
|
|
295
327
|
@click=${this.handleInputClick}
|
|
328
|
+
@keydown=${!this._hasTextInput ? this.handleSelectOnlyKeydown : nothing}
|
|
329
|
+
${ref(this.triggerRef)}
|
|
296
330
|
>
|
|
297
|
-
${this.
|
|
331
|
+
${!this._hasTextInput &&
|
|
332
|
+
this.placeholder &&
|
|
298
333
|
(!this._value.length || (this.multiple && this.tagPlacement == 'outside')) &&
|
|
299
334
|
!this._inputFocus
|
|
300
335
|
? html`<span class="pkt-combobox__placeholder" @click=${this.handlePlaceholderClick}
|
|
@@ -304,35 +339,14 @@ export class PktCombobox extends PktInputElement implements IPktCombobox {
|
|
|
304
339
|
? this.renderSingleOrMultipleValues()
|
|
305
340
|
: nothing}
|
|
306
341
|
${this.renderInputField()}
|
|
307
|
-
<
|
|
308
|
-
class
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
aria-
|
|
314
|
-
|
|
315
|
-
aria-haspopup="listbox"
|
|
316
|
-
aria-label="Åpne liste"
|
|
317
|
-
?disabled=${this.disabled}
|
|
318
|
-
?data-disabled=${this.disabled}
|
|
319
|
-
role="button"
|
|
320
|
-
tabindex="${this.disabled ? '-1' : '0'}"
|
|
321
|
-
>
|
|
322
|
-
<pkt-icon
|
|
323
|
-
class=${classMap({
|
|
324
|
-
'pkt-combobox__arrow-icon': true,
|
|
325
|
-
'pkt-combobox__arrow-icon--open': this._isOptionsOpen,
|
|
326
|
-
})}
|
|
327
|
-
name="chevron-thin-down"
|
|
328
|
-
></pkt-icon>
|
|
329
|
-
</div>
|
|
330
|
-
<div
|
|
331
|
-
${ref(this.focusRef)}
|
|
332
|
-
tabindex="-1"
|
|
333
|
-
@keydown=${this.handleArrowClick}
|
|
334
|
-
class="pkt-contents"
|
|
335
|
-
></div>
|
|
342
|
+
<pkt-icon
|
|
343
|
+
class=${classMap({
|
|
344
|
+
'pkt-combobox__arrow-icon': true,
|
|
345
|
+
'pkt-combobox__arrow-icon--open': this._isOptionsOpen,
|
|
346
|
+
})}
|
|
347
|
+
name="chevron-thin-down"
|
|
348
|
+
aria-hidden="true"
|
|
349
|
+
></pkt-icon>
|
|
336
350
|
</div>
|
|
337
351
|
|
|
338
352
|
<pkt-listbox
|
|
@@ -341,16 +355,17 @@ export class PktCombobox extends PktInputElement implements IPktCombobox {
|
|
|
341
355
|
.isOpen=${this._isOptionsOpen}
|
|
342
356
|
.searchPlaceholder=${this.searchPlaceholder}
|
|
343
357
|
.label="Liste: ${this.label || ''}"
|
|
344
|
-
?
|
|
345
|
-
?
|
|
346
|
-
?
|
|
347
|
-
?
|
|
358
|
+
?include-search=${this.includeSearch}
|
|
359
|
+
?is-multi-select=${this.multiple}
|
|
360
|
+
?allow-user-input=${this.allowUserInput && !this._maxIsReached}
|
|
361
|
+
?max-is-reached=${this._maxIsReached}
|
|
348
362
|
.customUserInput=${ifDefined(this._addValueText)}
|
|
349
363
|
.userMessage=${this._userInfoMessage}
|
|
350
364
|
@search=${this.handleSearch}
|
|
351
365
|
@option-toggle=${this.handleOptionToggled}
|
|
352
366
|
@select-all=${this.addAllOptions}
|
|
353
367
|
@close-options=${() => (this._isOptionsOpen = false)}
|
|
368
|
+
@tab-close=${() => this.closeAndProcessInput()}
|
|
354
369
|
.searchValue=${this._search || null}
|
|
355
370
|
.maxLength=${this.maxlength || 0}
|
|
356
371
|
${ref(this.listboxRef)}
|
|
@@ -366,7 +381,7 @@ export class PktCombobox extends PktInputElement implements IPktCombobox {
|
|
|
366
381
|
`
|
|
367
382
|
}
|
|
368
383
|
|
|
369
|
-
renderInputField() {
|
|
384
|
+
private renderInputField() {
|
|
370
385
|
return this.typeahead || this.allowUserInput
|
|
371
386
|
? html`
|
|
372
387
|
<div class="pkt-combobox__input-div combobox__input">
|
|
@@ -374,20 +389,31 @@ export class PktCombobox extends PktInputElement implements IPktCombobox {
|
|
|
374
389
|
type="text"
|
|
375
390
|
id="${this.id}-input"
|
|
376
391
|
name=${(this.name || this.id) + '-input'}
|
|
392
|
+
placeholder=${ifDefined(
|
|
393
|
+
!this._value.length || (this.multiple && this.tagPlacement === 'outside')
|
|
394
|
+
? this.placeholder
|
|
395
|
+
: undefined,
|
|
396
|
+
)}
|
|
377
397
|
@input=${this.handleInput}
|
|
398
|
+
@change=${(e: Event) => {
|
|
399
|
+
e.stopPropagation()
|
|
400
|
+
e.stopImmediatePropagation()
|
|
401
|
+
}}
|
|
378
402
|
@keydown=${this.handleInputKeydown}
|
|
379
403
|
@focus=${this.handleFocus}
|
|
380
404
|
@blur=${this.handleBlur}
|
|
381
405
|
autocomplete="off"
|
|
382
406
|
role="combobox"
|
|
407
|
+
aria-expanded=${this._isOptionsOpen ? 'true' : 'false'}
|
|
383
408
|
aria-label=${ifDefined(this.label)}
|
|
384
|
-
aria-autocomplete=${this.typeahead ? 'both' : 'list'}
|
|
409
|
+
aria-autocomplete=${this.typeahead ? 'both' : this.allowUserInput ? 'list' : 'none'}
|
|
385
410
|
aria-controls="${this.id}-listbox"
|
|
386
411
|
aria-activedescendant=${ifDefined(
|
|
387
|
-
this._value[0] && !!this.
|
|
388
|
-
? `${this.id}-listbox-${this.
|
|
412
|
+
this._value[0] && !!findOptionByValue(this.options, this._value[0])
|
|
413
|
+
? `${this.id}-listbox-${findOptionIndex(this._options, this._value[0])}`
|
|
389
414
|
: undefined,
|
|
390
415
|
)}
|
|
416
|
+
aria-description=${ifDefined(this._selectionDescription || undefined)}
|
|
391
417
|
${ref(this.inputRef)}
|
|
392
418
|
/>
|
|
393
419
|
</div>
|
|
@@ -403,39 +429,56 @@ export class PktCombobox extends PktInputElement implements IPktCombobox {
|
|
|
403
429
|
`
|
|
404
430
|
}
|
|
405
431
|
|
|
406
|
-
renderSingleOrMultipleValues() {
|
|
432
|
+
private renderSingleOrMultipleValues() {
|
|
433
|
+
// Single select with text input: value is shown in the input field, not as a span
|
|
434
|
+
if (!this.multiple && this._hasTextInput) return nothing
|
|
435
|
+
|
|
407
436
|
const isSingleValueDisplay = !this.multiple
|
|
408
437
|
|
|
409
|
-
//
|
|
410
|
-
const singleValueContent =
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
438
|
+
// Single value displayed as text (select-only mode)
|
|
439
|
+
const singleValueContent = this.renderValueTag(findOptionByValue(this.options, this._value[0]))
|
|
440
|
+
|
|
441
|
+
// Multiple values displayed as tags, wrapped in a list for accessibility
|
|
442
|
+
const isOutside = this.tagPlacement === 'outside'
|
|
443
|
+
const multipleValuesContent = html`
|
|
444
|
+
<ul role="list" class="pkt-combobox__tag-list">
|
|
445
|
+
${repeat(
|
|
446
|
+
this._value,
|
|
447
|
+
(value: string) => value,
|
|
448
|
+
(value: string, index: number) => {
|
|
449
|
+
const option = findOptionByValue(this.options, value)
|
|
450
|
+
const tagSkinColor = option?.tagSkinColor
|
|
451
|
+
return html`
|
|
452
|
+
<li
|
|
453
|
+
role="listitem"
|
|
454
|
+
@click=${isOutside ? nothing : (e: MouseEvent) => e.stopPropagation()}
|
|
455
|
+
@mousedown=${isOutside ? nothing : (e: MouseEvent) => e.preventDefault()}
|
|
456
|
+
>
|
|
457
|
+
<pkt-tag
|
|
458
|
+
skin=${tagSkinColor || 'blue-dark'}
|
|
459
|
+
?closeTag=${!this.disabled}
|
|
460
|
+
.buttonTabindex=${isOutside ? undefined : -1}
|
|
461
|
+
@close=${() => this.handleTagRemove(value)}
|
|
462
|
+
@keydown=${isOutside
|
|
463
|
+
? nothing
|
|
464
|
+
: (e: KeyboardEvent) => this.handleTagKeydown(e, index)}
|
|
465
|
+
>
|
|
466
|
+
${this.renderValueTag(option)}
|
|
467
|
+
</pkt-tag>
|
|
468
|
+
</li>
|
|
469
|
+
`
|
|
470
|
+
},
|
|
471
|
+
)}
|
|
472
|
+
</ul>
|
|
473
|
+
`
|
|
432
474
|
|
|
433
475
|
return isSingleValueDisplay ? singleValueContent : multipleValuesContent
|
|
434
476
|
}
|
|
435
477
|
|
|
436
|
-
renderValueTag(option: IPktComboboxOption | null) {
|
|
478
|
+
private renderValueTag(option: IPktComboboxOption | null) {
|
|
437
479
|
if (!option) return ''
|
|
438
|
-
|
|
480
|
+
const displayAs = this.displayValueAs as TPktComboboxDisplayValue
|
|
481
|
+
switch (displayAs) {
|
|
439
482
|
case 'prefixAndValue':
|
|
440
483
|
return html`<span class="pkt-combobox__value" data-focusfix=${this.id}
|
|
441
484
|
>${option.prefix || ''} ${option.value}</span
|
|
@@ -451,523 +494,6 @@ export class PktCombobox extends PktInputElement implements IPktCombobox {
|
|
|
451
494
|
>`
|
|
452
495
|
}
|
|
453
496
|
}
|
|
454
|
-
|
|
455
|
-
// Event handlers
|
|
456
|
-
|
|
457
|
-
handleInput(e: InputEvent): void {
|
|
458
|
-
e.stopPropagation()
|
|
459
|
-
e.stopImmediatePropagation()
|
|
460
|
-
|
|
461
|
-
if (this.disabled) return
|
|
462
|
-
|
|
463
|
-
this.touched = true
|
|
464
|
-
const input = e.target as HTMLInputElement
|
|
465
|
-
this._search = input.value
|
|
466
|
-
this.checkForMatches()
|
|
467
|
-
|
|
468
|
-
if (this.typeahead) {
|
|
469
|
-
if (this._search) {
|
|
470
|
-
this._options = this.options.filter((option) =>
|
|
471
|
-
option.fulltext?.toLowerCase().includes(this._search.toLowerCase()),
|
|
472
|
-
)
|
|
473
|
-
|
|
474
|
-
if (e.inputType !== 'deleteContentBackward') {
|
|
475
|
-
const matchingOptions = this._options.filter(
|
|
476
|
-
(option) =>
|
|
477
|
-
!option.selected &&
|
|
478
|
-
option.label?.toLowerCase().startsWith(this._search.toLowerCase()),
|
|
479
|
-
)
|
|
480
|
-
|
|
481
|
-
if (
|
|
482
|
-
matchingOptions.length > 0 &&
|
|
483
|
-
this.inputRef.value &&
|
|
484
|
-
this.inputRef.value.type !== 'hidden'
|
|
485
|
-
) {
|
|
486
|
-
const match = matchingOptions[0]
|
|
487
|
-
|
|
488
|
-
if (match?.label) {
|
|
489
|
-
input.value = match.label
|
|
490
|
-
window.setTimeout(
|
|
491
|
-
() => input.setSelectionRange(this._search.length, input.value.length),
|
|
492
|
-
0,
|
|
493
|
-
)
|
|
494
|
-
input.selectionDirection = 'backward'
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
} else {
|
|
499
|
-
this._options = [...this.options]
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
private handleFocus(): void {
|
|
505
|
-
if (this.disabled) return
|
|
506
|
-
|
|
507
|
-
if (
|
|
508
|
-
!this.multiple &&
|
|
509
|
-
this._value[0] &&
|
|
510
|
-
this.inputRef.value &&
|
|
511
|
-
this.inputRef.value.type !== 'hidden'
|
|
512
|
-
) {
|
|
513
|
-
const option = this.findValueInOptions(this._value[0])
|
|
514
|
-
this._editingSingleValue = true
|
|
515
|
-
this.inputRef.value.value =
|
|
516
|
-
this.displayValueAs === 'label' && option?.label ? option.label : this._value[0]
|
|
517
|
-
}
|
|
518
|
-
this._inputFocus = true
|
|
519
|
-
this._search = ''
|
|
520
|
-
this._options = [...this.options]
|
|
521
|
-
this._isOptionsOpen = true
|
|
522
|
-
this.onFocus()
|
|
523
|
-
|
|
524
|
-
this.requestUpdate()
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
private handleFocusOut(e: FocusEvent): void {
|
|
528
|
-
if (this.disabled) return
|
|
529
|
-
|
|
530
|
-
// Triggered when focus completely leaves the combobox and its children
|
|
531
|
-
if (
|
|
532
|
-
(e.relatedTarget as Element)?.closest('pkt-combobox')?.id !== this.id &&
|
|
533
|
-
(e.relatedTarget as Element)?.closest('pkt-combobox')?.id !== this.id &&
|
|
534
|
-
(e.target as Element)?.getAttribute('data-focusfix') !== this.id &&
|
|
535
|
-
e.relatedTarget !== this.focusRef.value &&
|
|
536
|
-
e.relatedTarget !== this.inputRef.value &&
|
|
537
|
-
e.relatedTarget !== this.arrowRef.value &&
|
|
538
|
-
this._isOptionsOpen
|
|
539
|
-
) {
|
|
540
|
-
this._inputFocus = false
|
|
541
|
-
this._addValueText = null
|
|
542
|
-
this._userInfoMessage = ''
|
|
543
|
-
this._search = ''
|
|
544
|
-
|
|
545
|
-
// If value in text input, check if it should be added
|
|
546
|
-
if (
|
|
547
|
-
this.inputRef.value &&
|
|
548
|
-
this.inputRef.value.type !== 'hidden' &&
|
|
549
|
-
this.inputRef.value.value !== ''
|
|
550
|
-
) {
|
|
551
|
-
const val = this.inputRef.value.value
|
|
552
|
-
const valInOptions = this.findValueInOptions(val)
|
|
553
|
-
if (!this._value.includes(val) && !valInOptions) {
|
|
554
|
-
if (this.allowUserInput) {
|
|
555
|
-
this.addNewUserValue(val)
|
|
556
|
-
} else if (!this.multiple) {
|
|
557
|
-
this.removeValue(this._value[0])
|
|
558
|
-
}
|
|
559
|
-
} else if (valInOptions && !this._value.includes(valInOptions.value)) {
|
|
560
|
-
this.setSelected(valInOptions.value)
|
|
561
|
-
}
|
|
562
|
-
this.inputRef.value.value = ''
|
|
563
|
-
}
|
|
564
|
-
this._isOptionsOpen = false
|
|
565
|
-
this.onBlur()
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
private handleBlur(): void {
|
|
570
|
-
this._inputFocus = false
|
|
571
|
-
this._editingSingleValue = false
|
|
572
|
-
this.onBlur()
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
private handleInputClick(e: MouseEvent): void {
|
|
576
|
-
if (this.disabled) {
|
|
577
|
-
e.preventDefault()
|
|
578
|
-
e.stopImmediatePropagation()
|
|
579
|
-
return
|
|
580
|
-
}
|
|
581
|
-
if (
|
|
582
|
-
e.currentTarget &&
|
|
583
|
-
e.currentTarget !== this.arrowRef.value &&
|
|
584
|
-
this.inputRef.value?.type !== 'hidden'
|
|
585
|
-
) {
|
|
586
|
-
this.inputRef.value?.focus()
|
|
587
|
-
this.requestUpdate()
|
|
588
|
-
} else {
|
|
589
|
-
this.handleArrowClick(e)
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
private handlePlaceholderClick(e: MouseEvent): void {
|
|
594
|
-
if (this.disabled) return
|
|
595
|
-
e.stopPropagation()
|
|
596
|
-
if (this.inputRef.value && this.inputRef.value.type !== 'hidden') {
|
|
597
|
-
this.inputRef.value.focus()
|
|
598
|
-
this._inputFocus = true
|
|
599
|
-
this.requestUpdate()
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
private handleArrowClick(e: MouseEvent | KeyboardEvent): void {
|
|
604
|
-
if (this.disabled) return
|
|
605
|
-
|
|
606
|
-
if (e instanceof KeyboardEvent && e.key) {
|
|
607
|
-
if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'ArrowDown') return
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
e.stopImmediatePropagation()
|
|
611
|
-
e.preventDefault()
|
|
612
|
-
|
|
613
|
-
this._isOptionsOpen = !this._isOptionsOpen
|
|
614
|
-
|
|
615
|
-
if (this._isOptionsOpen) {
|
|
616
|
-
this.listboxRef.value?.focusFirstOrSelectedOption()
|
|
617
|
-
} else {
|
|
618
|
-
this.arrowRef.value?.focus()
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
private handleOptionToggled(e: CustomEvent) {
|
|
623
|
-
this.toggleValue(e.detail)
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
private handleSearch(e: CustomEvent) {
|
|
627
|
-
e.stopPropagation()
|
|
628
|
-
this._search = e.detail.toLowerCase()
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
private handleInputKeydown(e: KeyboardEvent): void {
|
|
632
|
-
switch (e.key) {
|
|
633
|
-
case ',':
|
|
634
|
-
if (this.multiple) {
|
|
635
|
-
e.preventDefault()
|
|
636
|
-
this.addValue()
|
|
637
|
-
}
|
|
638
|
-
break
|
|
639
|
-
case 'Enter':
|
|
640
|
-
e.preventDefault()
|
|
641
|
-
this.addValue()
|
|
642
|
-
break
|
|
643
|
-
case 'Backspace':
|
|
644
|
-
if (!this._search && this.inputRef.value?.type === 'hidden') this.removeLastValue(e)
|
|
645
|
-
break
|
|
646
|
-
case 'Tab':
|
|
647
|
-
case 'ArrowDown':
|
|
648
|
-
if (!e.shiftKey) {
|
|
649
|
-
this.listboxRef.value?.focusFirstOrSelectedOption()
|
|
650
|
-
e.preventDefault()
|
|
651
|
-
}
|
|
652
|
-
break
|
|
653
|
-
case 'Escape':
|
|
654
|
-
this._isOptionsOpen = false
|
|
655
|
-
this.arrowRef.value?.focus()
|
|
656
|
-
e.preventDefault()
|
|
657
|
-
break
|
|
658
|
-
default:
|
|
659
|
-
break
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
private handleTagRemove(value: string | null): void {
|
|
664
|
-
this.removeSelected(value)
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
private blurInput(): void {
|
|
668
|
-
if (this.inputRef.value && this.inputRef.value.matches(':focus')) {
|
|
669
|
-
this.inputRef.value.blur()
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
private checkForMatches() {
|
|
674
|
-
// sjekker om verdiene bruker skriver inn finnes, er valgt eller kan legges til
|
|
675
|
-
//setter riktig infomelding til bruker
|
|
676
|
-
const inputValue = this.inputRef.value?.value || this._search || ''
|
|
677
|
-
const searchValue = inputValue.trim().toLowerCase() || ''
|
|
678
|
-
|
|
679
|
-
if (!searchValue) {
|
|
680
|
-
if (!this.multiple && this._value[0]) {
|
|
681
|
-
this.removeValue(this._value[0])
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
this.resetComboboxInput(false)
|
|
685
|
-
return
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
const matchedValues = this._value.find((value) => value.toLowerCase() === searchValue)
|
|
689
|
-
const matchedOptions: IPktComboboxOption[] = this._options.filter(
|
|
690
|
-
(option) => option.label?.toLowerCase().includes(searchValue) ?? false,
|
|
691
|
-
)
|
|
692
|
-
const matchedOption = matchedOptions.find(
|
|
693
|
-
(option) =>
|
|
694
|
-
option.label?.toLowerCase() === searchValue || option.value.toLowerCase() === searchValue,
|
|
695
|
-
)
|
|
696
|
-
|
|
697
|
-
// sett riktig infomelding til bruker
|
|
698
|
-
switch (true) {
|
|
699
|
-
case (matchedOptions.length === 0 || !matchedOption) && this.allowUserInput:
|
|
700
|
-
this._addValueText = inputValue
|
|
701
|
-
this._userInfoMessage = ''
|
|
702
|
-
break
|
|
703
|
-
|
|
704
|
-
case matchedOptions.length === 0 && !this._options.length && !this.allowUserInput:
|
|
705
|
-
this._addValueText = null
|
|
706
|
-
this._userInfoMessage = 'Ingen match i søket'
|
|
707
|
-
break
|
|
708
|
-
|
|
709
|
-
case !!matchedValues:
|
|
710
|
-
this._addValueText = null
|
|
711
|
-
this._userInfoMessage = 'Verdien er allerede valgt'
|
|
712
|
-
break
|
|
713
|
-
|
|
714
|
-
case matchedOptions.length > 1:
|
|
715
|
-
this._addValueText = null
|
|
716
|
-
this._userInfoMessage = ''
|
|
717
|
-
break
|
|
718
|
-
|
|
719
|
-
default:
|
|
720
|
-
this._addValueText = null
|
|
721
|
-
this._userInfoMessage = '' // Default for å fjerne melding
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
private findValueInOptions(value: string | null): IPktComboboxOption | null {
|
|
726
|
-
return (
|
|
727
|
-
this.options.find((option) => {
|
|
728
|
-
return option.value === value || option.label === value
|
|
729
|
-
}) || null
|
|
730
|
-
)
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
private findIndexInOptions(value: string | null): number {
|
|
734
|
-
return this._options.findIndex((option) => {
|
|
735
|
-
return option.value === value || option.label === value
|
|
736
|
-
})
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
private isMaxItemsReached(): boolean {
|
|
740
|
-
const isReached = this.maxlength !== null && this._value.length >= this.maxlength
|
|
741
|
-
if (!isReached) {
|
|
742
|
-
this._maxIsReached = false
|
|
743
|
-
} else {
|
|
744
|
-
this._maxIsReached = true
|
|
745
|
-
}
|
|
746
|
-
return isReached
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
public toggleValue(value: string | null): void {
|
|
750
|
-
if (this.disabled) return
|
|
751
|
-
|
|
752
|
-
this.touched = true
|
|
753
|
-
this._userInfoMessage = ''
|
|
754
|
-
this._addValueText = null
|
|
755
|
-
|
|
756
|
-
const valueFromOptions: string | null = this.findValueInOptions(value)?.value || null
|
|
757
|
-
const isSelected: boolean = this._value.includes(value || valueFromOptions || '')
|
|
758
|
-
const isInOption: boolean = !!valueFromOptions
|
|
759
|
-
const isDisabled: boolean = this._options.find((o) => o.value === value)?.disabled || false
|
|
760
|
-
const isEmpty: boolean = !value?.trim()
|
|
761
|
-
const isSingle: boolean = !this.multiple
|
|
762
|
-
const isMultiple: boolean = this.multiple
|
|
763
|
-
const isMaxItemsReached: boolean = this.isMaxItemsReached()
|
|
764
|
-
|
|
765
|
-
let shouldOptionsBeOpen: boolean = false
|
|
766
|
-
let shouldResetInput: boolean = true
|
|
767
|
-
let userInfoMessage: string | null = ''
|
|
768
|
-
let searchValue: string | null = ''
|
|
769
|
-
|
|
770
|
-
if (isDisabled) return
|
|
771
|
-
|
|
772
|
-
// Dersom ikke i listen og allowUserInput er true
|
|
773
|
-
if (!isInOption && this.allowUserInput && !isEmpty) {
|
|
774
|
-
this.addNewUserValue(value)
|
|
775
|
-
userInfoMessage = 'Ny verdi lagt til'
|
|
776
|
-
shouldOptionsBeOpen = !isMultiple
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
// Dersom ikke i listen men allowUserInput er false
|
|
780
|
-
else if (!isInOption && !this.allowUserInput) {
|
|
781
|
-
if (isSingle && this._value[0]) {
|
|
782
|
-
this.removeValue(this._value[0])
|
|
783
|
-
}
|
|
784
|
-
shouldResetInput = false
|
|
785
|
-
shouldOptionsBeOpen = true
|
|
786
|
-
userInfoMessage = 'Ingen treff i søket'
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// Dersom verdien er valgt allerede
|
|
790
|
-
else if (isSelected) {
|
|
791
|
-
this.removeValue(valueFromOptions)
|
|
792
|
-
shouldOptionsBeOpen = true
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
// Dersom verdien er en tom streng, og det er enkeltvalg
|
|
796
|
-
else if (isEmpty && isSingle) {
|
|
797
|
-
this.removeAllSelected()
|
|
798
|
-
shouldOptionsBeOpen = true
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
// Dersom det er enkeltvalg
|
|
802
|
-
else if (isSingle) {
|
|
803
|
-
this._value[0] && this.removeSelected(this._value[0])
|
|
804
|
-
this.setSelected(valueFromOptions)
|
|
805
|
-
shouldOptionsBeOpen = false
|
|
806
|
-
if (this.inputRef.value && this.inputRef.value.type !== 'hidden') {
|
|
807
|
-
this.inputRef.value.value = ''
|
|
808
|
-
this.inputRef.value.blur()
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
// Dersom det er flervalg og mulig å legge til fler
|
|
813
|
-
else if (isMultiple && !isMaxItemsReached) {
|
|
814
|
-
this.setSelected(valueFromOptions)
|
|
815
|
-
shouldOptionsBeOpen = true
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
// Dersom det er flervalg og maks antall er nådd
|
|
819
|
-
else if (isMultiple && isMaxItemsReached) {
|
|
820
|
-
this._userInfoMessage = 'Maks antall valg nådd'
|
|
821
|
-
shouldResetInput = false
|
|
822
|
-
searchValue = value
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
// Dersom ingen av de over passer
|
|
826
|
-
else {
|
|
827
|
-
isSingle && this.removeAllSelected()
|
|
828
|
-
this._userInfoMessage = 'Ingen gyldig verdi valgt'
|
|
829
|
-
shouldResetInput = false
|
|
830
|
-
shouldOptionsBeOpen = true
|
|
831
|
-
searchValue = value
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
this._isOptionsOpen = shouldOptionsBeOpen
|
|
835
|
-
if (!shouldOptionsBeOpen) {
|
|
836
|
-
window.setTimeout(() => {
|
|
837
|
-
this.focusRef.value?.focus()
|
|
838
|
-
}, 0)
|
|
839
|
-
}
|
|
840
|
-
this._userInfoMessage = userInfoMessage
|
|
841
|
-
this._search = searchValue || ''
|
|
842
|
-
this.resetComboboxInput(shouldResetInput)
|
|
843
|
-
isMultiple && this.isMaxItemsReached()
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
private setSelected(value: string | null): void {
|
|
847
|
-
if (this._value.includes(value as string)) return
|
|
848
|
-
|
|
849
|
-
if (this.multiple && this.isMaxItemsReached()) {
|
|
850
|
-
this._userInfoMessage = 'Maks antall valg nådd'
|
|
851
|
-
return
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
!this.multiple && this.removeAllSelected()
|
|
855
|
-
|
|
856
|
-
this._value = value ? [...this._value, value] : this._value
|
|
857
|
-
this._options = this._options.map((option) => {
|
|
858
|
-
if (option.value === value) {
|
|
859
|
-
option.selected = true
|
|
860
|
-
}
|
|
861
|
-
return option
|
|
862
|
-
})
|
|
863
|
-
|
|
864
|
-
this.resetComboboxInput(true)
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
private removeSelected(value: string | null): void {
|
|
868
|
-
if (!value) return
|
|
869
|
-
this._value = this._value.filter((v) => v !== value)
|
|
870
|
-
const _opt = this.findValueInOptions(value)
|
|
871
|
-
if (_opt) {
|
|
872
|
-
_opt.selected = false
|
|
873
|
-
if (_opt.userAdded) {
|
|
874
|
-
this._options = [...this._options.filter((o) => o.value !== value)]
|
|
875
|
-
this.options = [...this.options.filter((o) => o.value !== value)]
|
|
876
|
-
} else {
|
|
877
|
-
this._options = [...this._options, _opt]
|
|
878
|
-
}
|
|
879
|
-
} else if (!value && !this.multiple) {
|
|
880
|
-
this._options = this._options.map((option) => {
|
|
881
|
-
option.selected = false
|
|
882
|
-
return option
|
|
883
|
-
})
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
private addAllOptions(): void {
|
|
888
|
-
if (!this.multiple) return
|
|
889
|
-
if (this.maxlength && this._options.length > this.maxlength) {
|
|
890
|
-
this._userInfoMessage = 'For mange valgt'
|
|
891
|
-
return
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
this._value = this._options.map((option) => option.value)
|
|
895
|
-
this._options = this._options.map((option) => {
|
|
896
|
-
option.selected = true
|
|
897
|
-
return option
|
|
898
|
-
})
|
|
899
|
-
this.requestUpdate()
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
private removeAllSelected(): void {
|
|
903
|
-
this._value = []
|
|
904
|
-
this._options = this._options.map((option) => {
|
|
905
|
-
option.selected = false
|
|
906
|
-
return option
|
|
907
|
-
})
|
|
908
|
-
this._options = this._options.filter((option) => !option.userAdded)
|
|
909
|
-
this.requestUpdate()
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
private addValue(): void {
|
|
913
|
-
const input = this.inputRef.value?.value.trim() || ''
|
|
914
|
-
this._search = input
|
|
915
|
-
this.toggleValue(input)
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
private removeValue(value: string | null): void {
|
|
919
|
-
this._value = this.multiple ? this._value.filter((v) => v !== value) : []
|
|
920
|
-
this.removeSelected(value)
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
private addNewUserValue(value: string | null): void {
|
|
924
|
-
if (!value || value.trim() === '') return
|
|
925
|
-
|
|
926
|
-
if (!this.multiple) {
|
|
927
|
-
this._value[0] && this.removeSelected(this._value[0])
|
|
928
|
-
this._value = [value]
|
|
929
|
-
this._isOptionsOpen = false
|
|
930
|
-
this.blurInput()
|
|
931
|
-
} else if (!this.findValueInOptions(value)) {
|
|
932
|
-
if (this.isMaxItemsReached()) return
|
|
933
|
-
this._value = [...this._value, value]
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
const newOption: IPktComboboxOption = { value, label: value, userAdded: true }
|
|
937
|
-
|
|
938
|
-
this.options = [newOption, ...this.options]
|
|
939
|
-
this._options = [newOption, ...this._options]
|
|
940
|
-
this.setSelected(value)
|
|
941
|
-
this.requestUpdate()
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
private resetComboboxInput(shouldResetInput: boolean = true): void {
|
|
945
|
-
this._addValueText = null
|
|
946
|
-
if (this.inputRef.value && this.inputRef.value.type !== 'hidden' && shouldResetInput) {
|
|
947
|
-
this._search = ''
|
|
948
|
-
if (this.multiple) {
|
|
949
|
-
this.inputRef.value.value = ''
|
|
950
|
-
} else {
|
|
951
|
-
const option = this.findValueInOptions(this._value[0])
|
|
952
|
-
window.setTimeout(() => {
|
|
953
|
-
if (!this.inputRef.value || this.inputRef.value.type === 'hidden') return
|
|
954
|
-
this.inputRef.value.value =
|
|
955
|
-
this.displayValueAs === 'label' && option?.label ? option.label : this._value[0] || ''
|
|
956
|
-
}, 0)
|
|
957
|
-
this._userInfoMessage = ''
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
this._options = [...this.options]
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
private removeLastValue(e: Event): void {
|
|
964
|
-
if (this._value.length === 0) return
|
|
965
|
-
|
|
966
|
-
e.preventDefault()
|
|
967
|
-
|
|
968
|
-
const val = this._value[this._value.length - 1]
|
|
969
|
-
val && this.removeSelected(val)
|
|
970
|
-
|
|
971
|
-
this.isMaxItemsReached()
|
|
972
|
-
}
|
|
973
497
|
}
|
|
498
|
+
|
|
499
|
+
export default PktCombobox
|