blob: 5119ab71f0de0cf1626fdf25903eefa83a641a4f [file] [log] [blame]
/**
* @module modules/picker-field-sk
* @description <h2><code>picker-field-sk</code></h2>
*
* A stylized text field that let's the user pick a value from a pre-selected
* list of valid options. Specifically, designed to be used by the new Test
* Picker component, but may be reused for other purposes.
*
* @evt value-changed - This event gets triggered whenever a text field value
* is selected. Clearing the text field counts as selecting a value. The
* value will be available in event.detail.value.
*
* @attr {string} label - The label to be used on top of the text field and as
* a placeholder in the text field.
*
* @attr {string[]} options - A valid selection of options that'll be displayed
* as options to the user in the dropdown menu.
*
* @example
* <picker-field-sk
* .label="benchmark"
* .options=${["Speedometer2", "Jetstream2"]}
* >
* </picker-field-sk>
*/
import { html } from 'lit/html.js';
import { define } from '../../../elements-sk/modules/define';
import { ElementSk } from '../../../infra-sk/modules/ElementSk';
import { CheckOrRadio } from '../../../elements-sk/modules/checkbox-sk/checkbox-sk';
import '@vaadin/multi-select-combo-box/theme/lumo/vaadin-multi-select-combo-box.js';
export interface SplitChartSelectionEventDetails {
attribute: string;
}
export class PickerFieldSk extends ElementSk {
private _label: string = '';
private _helper_text: string = '';
private _options: string[] = [];
private _comboBox: HTMLElement | null = null;
private _splitBox: CheckOrRadio | null = null;
private _allSelected: CheckOrRadio | null = null;
private _primarySelected: CheckOrRadio | null = null;
private _selectedItems: string[] = [];
private _primaryOptions: string[] = [];
private _split: boolean = false;
private _index: number = 0;
private _checkboxSelected: boolean = false;
/**
* Creates an instance of PickerFieldSk.
* @param label The label for the picker field.
*/
constructor(label: string) {
super(PickerFieldSk.template);
this._label = label;
}
private static template = (ele: PickerFieldSk) => html`
<div id="picker-field-${ele.label}">
<div id="split-by-container">
<checkbox-sk
title="Split the chart by attribute."
name=${ele.label}
id="split-by"
label="Split"
@change=${ele.splitOnValue}
?checked=${ele.split}
?hidden=${!ele.showSplit}>
</checkbox-sk>
<checkbox-sk
title="Select all values without periods in the name."
name=${ele.label}
id="select-primary"
label="Primary"
@change=${ele.selectPrimary}
?checked=${ele._arePrimarySelected}
?hidden=${!ele.showPrimary}>
</checkbox-sk>
<checkbox-sk
title="Select All"
name=${ele.label}
id="select-all"
label="All"
@change=${ele.selectAll}
?checked=${ele._isAllSelected}
?hidden=${!ele.showSelectAll}>
</checkbox-sk>
</div>
<vaadin-multi-select-combo-box
auto-expand-vertically
label=${ele.label}
.items=${ele.options}
.selectedItems=${ele.selectedItems}
@selected-items-changed=${ele.onValueChanged}
selected-items-on-top>
</vaadin-multi-select-combo-box>
</div>
`;
/**
* Handles the 'selected-items-changed' event from the
* vaadin-multi-select-combo-box.
* Dispatches a custom 'value-changed' event with the new selected items.
* @param e The event object.
*/
private onValueChanged(e: Event) {
const selectedItems = (e as CustomEvent).detail.value as string[];
this.dispatchEvent(
new CustomEvent('value-changed', {
detail: {
value: selectedItems, // Forward the array of selected items
checkboxSelected: this._checkboxSelected,
},
bubbles: true,
composed: true,
})
);
}
/**
* Handles the change event for the "Split By" checkbox.
* It updates the `_splitBy` property and dispatches a custom event
* to notify that the split option has changed.
*
* @param e - The event triggered by the checkbox change.
*/
private splitOnValue(e: Event) {
this._split = (e.currentTarget as HTMLInputElement).checked;
this.dispatchEvent(
new CustomEvent('split-by-changed', {
detail: {
param: this.label,
split: this._split,
},
bubbles: true,
composed: true,
})
);
}
/**
* Selects all options if the "Select All" checkbox is checked.
* If it is unchecked, it clears the selection.
*
* @param e - The event triggered by the checkbox change.
*/
private selectAll(e: Event) {
if (this._allSelected) {
this._checkboxSelected = true;
if ((e.currentTarget as HTMLInputElement).checked) {
// Create a shallow copy to avoid direct mutation issues.
this.selectedItems = this.options.slice();
} else {
// Leave the first item selected.
this.selectedItems = this.options.slice(0, 1);
}
}
}
/**
* Selects or unselects primary options based on the checkbox state.
* If checked, it adds all primary options to the current selection.
* If unchecked, it removes all primary options from the current selection.
*/
private selectPrimary(e: Event) {
if (this._primarySelected) {
this._checkboxSelected = true;
if ((e.currentTarget as HTMLInputElement).checked) {
// If all is selected, deselect all and select primary.
if (this._isAllSelected) {
this.selectedItems = [];
}
// Add all primary options to the current selection, ensuring no duplicates.
const newSelection = [...new Set([...this.selectedItems, ...this.primaryOptions])];
this.selectedItems = newSelection;
} else {
// Leave the first item selected.
this.selectedItems = this.options.slice(0, 1);
}
}
}
connectedCallback(): void {
super.connectedCallback();
this._render();
this._comboBox = this.querySelector('vaadin-multi-select-combo-box');
this._splitBox = this.querySelector('checkbox-sk#split-by');
this._allSelected = this.querySelector('checkbox-sk#select-all');
this._primarySelected = this.querySelector('checkbox-sk#select-primary');
}
/**
* Sets focus on the internal vaadin-multi-select-combo-box element.
*/
focus() {
if (this._comboBox !== null) {
this._comboBox!.focus();
}
this._render();
}
/**
* Opens the overlay (dropdown) of the internal
* vaadin-multi-select-combo-box element.
*/
openOverlay() {
if (this._comboBox !== null) {
this._comboBox!.click();
}
this._render();
}
/**
* Disables the picker field and associated checkboxes.
* Sets the vaadin-multi-select-combo-box to readonly and disables the
* "Select All", "Split", and "Primary" checkboxes.
*/
disable() {
if (this._comboBox !== null) {
this._comboBox!.setAttribute('readonly', '');
if (this._allSelected !== null) {
this._allSelected.disabled = true;
}
if (this._splitBox !== null) {
this._splitBox.disabled = true;
}
if (this._primarySelected !== null) {
this._primarySelected.disabled = true;
}
this._render();
}
}
/**
* Enables the picker field and associated checkboxes.
* Removes the readonly attribute from the vaadin-multi-select-combo-box
* and enables the "Select All", "Split", and "Primary" checkboxes.
*/
enable() {
if (this._comboBox !== null) {
this._comboBox!.removeAttribute('opened');
this._comboBox!.removeAttribute('readonly');
if (this._allSelected !== null) {
this._allSelected.disabled = false;
}
if (this._splitBox !== null) {
this._splitBox.disabled = false;
}
if (this._primarySelected !== null) {
this._primarySelected.disabled = false;
}
this._render();
}
}
/**
* Clears the selected value of the combo box and resets the selected items.
*/
clear() {
this.focus();
this.setValue('');
this.selectedItems = [];
}
/**
* Sets the value of the internal vaadin-multi-select-combo-box element.
* @param val The string value to set.
*/
setValue(val: string) {
this._comboBox!.removeAttribute('value');
this._comboBox!.setAttribute('value', val);
this._render();
}
/**
* Enables the split checkbox.
*/
enableSplit() {
if (this._splitBox !== null) {
this._splitBox.removeAttribute('disabled');
}
this._render();
}
/**
* Disables the split checkbox and sets the split property to false.
*/
disableSplit() {
if (this._splitBox !== null) {
this._split = false;
this._splitBox.setAttribute('disabled', '');
}
this._render();
}
/**
* Resets the selected items of the combo box.
*/
reset() {
this.selectedItems = [];
}
/**
* Set the overlay width based on the ComboBox's options.
*
* Calculate the longest string from the options. Then set the
* width property to length + 5 (padding) "ch" (character width unit).
*
*/
private calculateOverlayWidth() {
let maxLength = 0;
this.options.forEach((option) => {
if (option.length > maxLength) {
maxLength = option.length;
}
});
const width = `${maxLength + 5}ch`;
if (this._comboBox !== null) {
this._comboBox!.style.setProperty('--vaadin-multi-select-combo-box-overlay-width', width);
this._comboBox!.style.width = width;
}
}
/**
* Returns true if all options are currently selected.
* @returns True if all options are selected, false otherwise.
*/
private get _isAllSelected(): boolean {
if (this._options.length === 0) {
return false;
}
return this.selectedItems.length === this._options.length;
}
/**
* Returns true if all primary options are currently selected.
* @returns True if all primary options are selected, false otherwise.
*/
private get _arePrimarySelected(): boolean {
if (this._primaryOptions.length === 0) {
return false;
}
const show = this._primaryOptions.every((p) => this.selectedItems.includes(p));
return this._primaryOptions.length === this.selectedItems.length && show;
}
/**
* Gets the index of the picker field.
* @returns The index of the picker field.
*/
get index(): number {
return this._index;
}
/**
* Sets the index of the picker field.
* @param v The new index value.
*/
set index(v: number) {
this._index = v;
}
/**
* Gets the current split state of the picker field.
* @returns True if the field is split, false otherwise.
*/
get split(): boolean {
return this._split;
}
/**
* Sets the split state of the picker field.
* Updates the checked state of the split checkbox.
* @param v The new split state.
*/
set split(v: boolean) {
this._split = v;
if (this._splitBox !== null) {
this._splitBox.checked = v;
}
this._render();
}
/**
* Gets the array of available options for the picker field.
* @returns An array of strings representing the available options.
*/
get options(): string[] {
return this._options;
}
/**
* Sets the array of available options for the picker field.
* Also filters primary options and recalculates the overlay width.
* @param v The new array of options.
*/
set options(v: string[]) {
this._options = v;
this.primaryOptions = v.filter((option) => !option.includes('.'));
this.calculateOverlayWidth();
this._render();
}
/**
* Gets the array of primary options (options without periods) for the
* picker field.
* @returns An array of strings representing the primary options.
*/
get primaryOptions(): string[] {
return this._primaryOptions;
}
/**
* Sets the array of primary options for the picker field.
* @param v The new array of primary options.
*/
set primaryOptions(v: string[]) {
this._primaryOptions = v;
this._render();
}
/**
* Gets the label of the picker field.
* @returns The label of the picker field.
*/
get label(): string {
return this._label;
}
/**
* Sets the label of the picker field.
* @param v The new label value.
*/
set label(v: string) {
this._label = v;
this._render();
}
/**
* Gets the currently selected items of the combo box.
* @returns An array of strings representing the selected items.
*/
get selectedItems(): string[] {
return this._selectedItems;
}
/**
* Sets the selected items of the combo box.
* @param v The new array of selected items.
*/
set selectedItems(v: string[]) {
this._selectedItems = v;
this._render();
}
/**
* Gets the helper text of the picker field.
* @returns The helper text of the picker field.
*/
get helperText(): string {
return this._helper_text;
}
/**
* Sets the helper text of the picker field.
* @param v The new helper text value.
*/
set helperText(v: string) {
this._helper_text = v;
this._render();
}
/**
* Returns true if the "Select All" checkbox should be shown.
* The checkbox is shown if there are more than 2 options and the field is
* not the first field (index > 0).
* @returns True if the "Select All" checkbox should be shown, false
* otherwise.
*/
get showSelectAll(): boolean {
return this.options.length > 2 && this.index > 0;
}
/**
* Returns true if the "Split" checkbox should be shown.
* The checkbox is shown if there are more than 1 selected item and the
* field is not the first field (index > 0).
* @returns True if the "Split" checkbox should be shown, false otherwise.
*/
get showSplit(): boolean {
return this.selectedItems.length > 1 && this.index > 0;
}
/**
* Returns true if the "Primary" checkbox should be shown.
* The checkbox is shown if the number of primary options is different from
* the total number of options and the field is not the first field
* (index > 0).
* @returns True if the "Primary" checkbox should be shown, false otherwise.
*/
get showPrimary(): boolean {
return (
this.primaryOptions.length > 0 &&
this.primaryOptions.length !== this.options.length &&
this.index > 0
);
}
}
define('picker-field-sk', PickerFieldSk);