Files
rio/frontend/code/components/switcherBar.ts
2024-06-20 10:18:48 +02:00

195 lines
6.2 KiB
TypeScript

import { ComponentBase, ComponentState } from './componentBase';
import { ColorSet } from '../dataModels';
import { applySwitcheroo } from '../designApplication';
import { markEventAsHandled } from '../eventHandling';
export type SwitcherBarState = ComponentState & {
_type_: 'SwitcherBar-builtin';
names?: string[];
icon_svg_sources?: (string | null)[];
color?: ColorSet;
orientation?: 'horizontal' | 'vertical';
spacing?: number;
allow_none: boolean;
selectedName?: string | null;
};
export class SwitcherBarComponent extends ComponentBase {
state: Required<SwitcherBarState>;
private optionsContainer: HTMLElement;
private markerElement: HTMLElement;
private selectedOptionElement: HTMLElement | null = null;
createElement(): HTMLElement {
// Create the elements
let element = document.createElement('div');
element.classList.add('rio-switcher-bar');
// Highlights the selected item
this.markerElement = document.createElement('div');
this.markerElement.classList.add('rio-switcher-bar-marker');
element.appendChild(this.markerElement);
this.optionsContainer = document.createElement('div');
this.optionsContainer.classList.add(
'rio-switcher-bar-options-container'
);
element.appendChild(this.optionsContainer);
return element;
}
updateElement(
deltaState: SwitcherBarState,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);
// Have the options changed?
if (
deltaState.names !== undefined ||
deltaState.icon_svg_sources !== undefined
) {
this.rebuildOptions(
deltaState.names ?? this.state.names,
deltaState.icon_svg_sources ?? this.state.icon_svg_sources
);
// Make sure the newly created option element is properly selected
this.selectedOptionElement = null;
if (deltaState.selectedName === undefined) {
deltaState.selectedName = this.state.selectedName;
}
}
// Color
if (deltaState.color !== undefined) {
applySwitcheroo(
this.markerElement,
deltaState.color === 'keep' ? 'bump' : deltaState.color
);
}
// Orientation
if (deltaState.orientation !== undefined) {
this.optionsContainer.style.flexDirection =
deltaState.orientation == 'vertical' ? 'column' : 'row';
}
// Spacing
if (deltaState.spacing !== undefined) {
this.optionsContainer.style.gap = `${deltaState.spacing}rem`;
}
// If the selection has changed make sure to move the marker
if (deltaState.selectedName !== undefined) {
if (deltaState.selectedName === null) {
this.select(null);
} else {
let i = (deltaState.names ?? this.state.names).indexOf(
deltaState.selectedName
);
this.select(i);
}
}
}
private rebuildOptions(
names: string[],
iconSvgSources: (string | null)[]
): void {
this.optionsContainer.innerHTML = '';
for (let [index, name] of names.entries()) {
let iconSvg = iconSvgSources[index];
let optionElement = this.buildOptionElement(index, name, iconSvg);
this.optionsContainer.appendChild(optionElement);
}
}
private buildOptionElement(
index: number,
name: string,
iconSvg: string | null
): HTMLElement {
let optionElement = document.createElement('div');
optionElement.classList.add('rio-switcher-bar-option');
optionElement.style.justifyContent = 'center';
// Icon
if (iconSvg !== null) {
optionElement.innerHTML = iconSvg;
// `space-between` looks ugly if there's only a single child (the
// child is at the top instead of centered), so only use that if we
// have an icon *and* text
optionElement.style.justifyContent = 'space-between';
}
// Text
let textElement = document.createElement('div');
optionElement.appendChild(textElement);
textElement.textContent = name;
// Detect clicks
optionElement.addEventListener('click', (event) => {
// If this item was already selected, the new value may be `None`
if (this.state.selectedName === name) {
if (this.state.allow_none) {
this.state.selectedName = null;
this.select(null);
} else {
return;
}
} else {
this.state.selectedName = name;
this.select(index);
}
// Notify the backend
this.sendMessageToBackend({
name: this.state.selectedName,
});
// Eat the event
markEventAsHandled(event);
});
return optionElement;
}
private select(index: number | null): void {
if (this.selectedOptionElement !== null) {
this.selectedOptionElement.classList.remove('selected');
}
if (index === null) {
this.selectedOptionElement = null;
this.markerElement.style.width = '0';
this.markerElement.style.height = '0';
return;
}
let optionElement = this.optionsContainer.children[
index
] as HTMLElement;
optionElement.classList.add('selected');
this.selectedOptionElement = optionElement;
let optionRect = optionElement.getBoundingClientRect();
let containerRect = this.optionsContainer.getBoundingClientRect();
this.markerElement.style.left = `${
optionRect.left - containerRect.left
}px`;
this.markerElement.style.top = `${
optionRect.top - containerRect.top
}px`;
this.markerElement.style.width = `${optionRect.width}px`;
this.markerElement.style.height = `${optionRect.height}px`;
}
}