import { html, PropertyValues } from 'lit' import { customElement, property } from 'lit/decorators.js' import { Ref, createRef, ref } from 'lit/directives/ref.js' import { ifDefined } from 'lit/directives/if-defined.js' import { PktOptionsInputElement } from '@/base-elements/options-input-element' import { PktOptionsSlotController } from '@/controllers/pkt-options-controller' import { slotContent } from '@/directives/slot-content' import type { TSelectOption } from 'shared-types/select' import '@/components/input-wrapper' export type { TSelectOption } from 'shared-types/select' export interface IPktSelect { options: TSelectOption[] value: string } /** * Pkt Select is a wrapper for the native select element using the pkt-input-wrapper component. * * The component will prioritize options passed as a prop over options passed as children if both are provided. * This is to allow for dynamic options that might change in the case of both children/slot and props are provided. * * @slot (default) - Options to be rendered as children * @prop {TSelectOption[]} options - Options to be rendered as children * * */ declare global { interface HTMLElementTagNameMap { 'pkt-select': PktSelect & HTMLSelectElement } } export class PktSelect extends PktOptionsInputElement<{}, TSelectOption> implements IPktSelect { inputRef: Ref = createRef() @property({ type: String }) value: string = '' public selectedIndex: number | null = -1 public selectedOptions: HTMLCollectionOf | undefined = undefined constructor() { super() this.optionsController = new PktOptionsSlotController(this) } connectedCallback(): void { super.connectedCallback() // Parse options from props or slots this.parseOptions() // Set initial value from selected option this._options.forEach((option) => { if (option.selected && !this.value) { this.value = option.value } }) } // Support native Select method `add` public add(item: HTMLOptionElement, before?: HTMLOptionElement | number) { const newOption = { value: item.value || item.text, label: item.text || item.value, selected: item.selected, disabled: item.disabled, } // Add to our internal options array at the correct position if (before === undefined) { this._options.push(newOption) } else if (typeof before === 'number') { this._options.splice(before, 0, newOption) } else { // If before is an HTMLOptionElement, find its index in our options const beforeValue = before.value || before.text const beforeIndex = this._options.findIndex((opt) => opt.value === beforeValue) if (beforeIndex >= 0) { this._options.splice(beforeIndex, 0, newOption) } else { this._options.push(newOption) } } if (item.selected) { this.value = item.value || item.text this.selectedIndex = this._options.findIndex((opt) => opt.value === this.value) } this.requestUpdate() } // Support native Select method `remove` public remove(item?: number) { if (typeof item === 'number') { if (this.selectedIndex === item) { this.value = this._options[0]?.value || '' } this._options.splice(item, 1) this.requestUpdate() } } // Support native Select method `item` public item(index: number) { return this.inputRef.value?.item(index) } // Support native Select method `namedItem` public namedItem(name: string) { return this.inputRef.value?.namedItem(name) } // Support native Select method `showPicker` public showPicker() { if (this.inputRef.value && 'showPicker' in this.inputRef.value) { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- showPicker is not in TypeScript's HTMLSelectElement type yet ;(this.inputRef.value as any).showPicker() } } public attributeChangedCallback(name: string, _old: string, value: string): void { if (name === 'options') { this._options = value ? JSON.parse(value) : [] } if (name === 'value' && this.value !== _old) { this.selectedIndex = this.touched ? this.returnNumberOrNull(this.inputRef.value?.selectedIndex) : this._options.findIndex((option) => option.value === value) this.selectedOptions = this.inputRef.value?.selectedOptions this.valueChanged(value, _old) } super.attributeChangedCallback(name, _old, value) } update(changedProperties: PropertyValues) { super.update(changedProperties) if (changedProperties.has('_optionsProp') && this._optionsProp.length > 0) { this._options = this._optionsProp this.requestUpdate('_options') // If no value is set and we have options, set to first option if (!this.value && this._options.length > 0) { // eslint-disable-next-line lit/no-property-change-update -- Initial value setup is intentional this.value = this._options[0].value this.selectedIndex = 0 } } if (changedProperties.has('value') && this.value !== changedProperties.get('value')) { this.selectedIndex = this.touched ? this.returnNumberOrNull(this.inputRef.value?.selectedIndex) : this._options.findIndex((option) => option.value === this.value) this.selectedOptions = this.inputRef.value?.selectedOptions this.valueChanged(this.value, changedProperties.get('value')) } if (changedProperties.has('id')) { !this.name && this.id && (this.name = this.id) } } protected firstUpdated(_changedProperties: PropertyValues): void { super.firstUpdated(_changedProperties) if (this._optionsProp.length) { this._options = this._optionsProp } // If no options are selected, set value and selectedIndex to the first option if (!this.value && this._options.length > 0) { this.value = this._options[0].value this.selectedIndex = 0 } else { this.selectedIndex = this._options.findIndex((option) => option.value === this.value) } this.selectedOptions = this.inputRef.value?.selectedOptions } render() { const inputClass = `pkt-input pkt-input--${this.inputSize} ${this.fullwidth ? 'pkt-input--fullwidth' : ''}` return html`
${slotContent(this, 'helptext')}
` } private returnNumberOrNull(value: number | null | undefined): number | null { if (value === undefined || value === null || isNaN(value)) { return null } return value } } try { customElement('pkt-select')(PktSelect) } catch (e) { console.warn('Forsøker å definere , men den er allerede definert') }