choices.js
Version:
A vanilla JS customisable text input/select box plugin
116 lines (101 loc) • 4.05 kB
text/typescript
import { parseCustomProperties } from '../lib/utils';
import { ClassNames } from '../interfaces/class-names';
import WrappedElement from './wrapped-element';
import { GroupFull } from '../interfaces/group-full';
import { ChoiceFull } from '../interfaces/choice-full';
import { stringToHtmlClass } from '../lib/choice-input';
import { isHtmlOptgroup, isHtmlOption } from '../lib/html-guard-statements';
export default class WrappedSelect extends WrappedElement<HTMLSelectElement> {
classNames: ClassNames;
template: (data: object) => HTMLOptionElement;
extractPlaceholder: boolean;
constructor({
element,
classNames,
template,
extractPlaceholder,
}: {
element: HTMLSelectElement;
classNames: ClassNames;
template: (data: object) => HTMLOptionElement;
extractPlaceholder: boolean;
}) {
super({ element, classNames });
this.template = template;
this.extractPlaceholder = extractPlaceholder;
}
get placeholderOption(): HTMLOptionElement | null {
return (
this.element.querySelector('option[value=""]') ||
// Backward compatibility layer for the non-standard placeholder attribute supported in older versions.
this.element.querySelector('option[placeholder]')
);
}
addOptions(choices: ChoiceFull[]): void {
const fragment = document.createDocumentFragment();
choices.forEach((obj) => {
const choice = obj;
if (choice.element) {
return;
}
const option = this.template(choice);
fragment.appendChild(option);
choice.element = option;
});
this.element.appendChild(fragment);
}
optionsAsChoices(): (ChoiceFull | GroupFull)[] {
const choices: (ChoiceFull | GroupFull)[] = [];
this.element.querySelectorAll<HTMLElement>(':scope > option, :scope > optgroup').forEach((e) => {
if (isHtmlOption(e)) {
choices.push(this._optionToChoice(e));
} else if (isHtmlOptgroup(e)) {
choices.push(this._optgroupToChoice(e));
}
// todo: hr as empty optgroup, requires displaying empty opt-groups to be useful
});
return choices;
}
// eslint-disable-next-line class-methods-use-this
_optionToChoice(option: HTMLOptionElement): ChoiceFull {
// option.value returns the label if there is no value attribute, which can break legacy placeholder attribute support
if (!option.hasAttribute('value') && option.hasAttribute('placeholder')) {
option.setAttribute('value', '');
option.value = '';
}
return {
id: 0,
group: null,
score: 0,
rank: 0,
value: option.value,
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/option
// This attribute is text for the label indicating the meaning of the option. If the `label` attribute isn't defined, its value is that of the element text content (ie `innerText`).
label: option.label,
element: option,
active: true,
// this returns true if nothing is selected on initial load, which will break placeholder support
selected: this.extractPlaceholder ? option.selected : option.hasAttribute('selected'),
disabled: option.disabled,
highlighted: false,
placeholder: this.extractPlaceholder && (!option.value || option.hasAttribute('placeholder')),
labelClass:
typeof option.dataset.labelClass !== 'undefined' ? stringToHtmlClass(option.dataset.labelClass) : undefined,
labelDescription:
typeof option.dataset.labelDescription !== 'undefined' ? option.dataset.labelDescription : undefined,
customProperties: parseCustomProperties(option.dataset.customProperties),
};
}
_optgroupToChoice(optgroup: HTMLOptGroupElement): GroupFull {
const options = optgroup.querySelectorAll('option');
const choices = Array.from(options).map((option) => this._optionToChoice(option));
return {
id: 0,
label: optgroup.label || '',
element: optgroup,
active: !!choices.length,
disabled: optgroup.disabled,
choices,
};
}
}