import { subscriberCollection, type AccessorType, type ICollectionObserver, type IObserverLocator, type ISubscriberCollection, } from '@aurelia/runtime'; import type { INode } from '../dom.node'; import { atLayout, atNode, atObserver, hasOwnProperty } from '../utilities'; import { INodeObserver, INodeObserverConfigBase } from './observer-locator'; import { mixinNodeObserverUseConfig } from './observation-utils'; import { createMutationObserver } from '../utilities-dom'; import { ErrorNames, createMappedError } from '../errors'; import { isArray } from '@aurelia/kernel'; export interface ISelectElement extends HTMLSelectElement { options: HTMLCollectionOf & Pick; matcher?: (a: unknown, b: unknown) => boolean; } export interface IOptionElement extends HTMLOptionElement { model?: unknown; } export interface SelectValueObserver extends ISubscriberCollection {} export class SelectValueObserver implements INodeObserver { static { mixinNodeObserverUseConfig(SelectValueObserver); subscriberCollection(SelectValueObserver, null!); } /** @internal */ private static _getSelectedOptions(options: ArrayLike): unknown[] { const selection: unknown[] = []; if (options.length === 0) { return selection; } const ii = options.length; let i = 0; let option: IOptionElement; while (ii > i) { option = options[i]; if (option.selected) { selection[selection.length] = hasOwnProperty.call(option, 'model') ? option.model : option.value; } ++i; } return selection; } /** @internal */ private static _defaultMatcher(a: unknown, b: unknown): boolean { return a === b; } // ObserverType.Layout is not always true // but for simplicity, always treat as such public type: AccessorType = (atNode | atObserver | atLayout) as AccessorType; /** @internal */ private _value: unknown = void 0; /** @internal */ private _oldValue: unknown = void 0; /** @internal */ public readonly _el: ISelectElement; /** @internal */ private _hasChanges: boolean = false; /** @internal */ private _arrayObserver?: ICollectionObserver<'array'> = void 0; /** @internal */ private _nodeObserver?: MutationObserver = void 0; /** @internal */ private _observing: boolean = false; /** @internal */ private readonly _observerLocator: IObserverLocator; /** * Used by mixing defined methods subscribe/unsubscribe * * @internal */ public _listened: boolean = false; /** @internal */ public _config: INodeObserverConfigBase; /** * Comes from mixin */ public useConfig!: (config: INodeObserverConfigBase) => void; public constructor( obj: INode, // deepscan-disable-next-line _key: PropertyKey, config: INodeObserverConfigBase, observerLocator: IObserverLocator, ) { this._el = obj as ISelectElement; this._observerLocator = observerLocator; this._config = config; } public getValue(): unknown { // is it safe to assume the observer has the latest value? // todo: ability to turn on/off cache based on type return this._observing ? this._value : this._el.multiple // todo: maybe avoid double iteration? ? SelectValueObserver._getSelectedOptions(this._el.options) : this._el.value; } public setValue(newValue: unknown): void { this._oldValue = this._value; this._value = newValue; this._hasChanges = newValue !== this._oldValue; this._observeArray(newValue instanceof Array ? newValue : null); this._flushChanges(); } /** @internal */ private _flushChanges(): void { if (this._hasChanges) { this._hasChanges = false; this.syncOptions(); } } public handleCollectionChange(): void { // always sync "selected" property of // immediately whenever the array notifies its mutation this.syncOptions(); } public syncOptions(): void { const value = this._value; const obj = this._el; const $isArray = isArray(value); const matcher = obj.matcher ?? SelectValueObserver._defaultMatcher; const options = obj.options; let i = options.length; while (i-- > 0) { const option = options[i]; const optionValue = hasOwnProperty.call(option, 'model') ? option.model : option.value; if ($isArray) { option.selected = value.findIndex(item => !!matcher(optionValue, item)) !== -1; continue; } option.selected = !!matcher(optionValue, value); } } public syncValue(): boolean { // Spec for synchronizing value from ` element, do the following steps: // A. If `