PlanTempusApp/PlanTempus.Application/wwwroot/ts/modules/controls.ts
Janus C. H. Knudsen e9f3639c7c Enhance service detail UI with improved select and color controls
Refactors select dropdown functionality to use custom implementation
Adds color dot support for color selection
Improves keyboard navigation and interaction for select dropdowns

Modernizes UI components with more flexible and interactive controls
2026-01-16 23:25:05 +01:00

144 lines
4.4 KiB
TypeScript

/**
* Controls Module
*
* Handles generic UI controls functionality:
* - Toggle sliders (Ja/Nej switches)
* - Select dropdowns
*/
/**
* Controller for generic UI controls
*/
export class ControlsController {
constructor() {
this.initToggleSliders();
this.initSelectDropdowns();
}
/**
* Initialize all toggle sliders on the page
* Toggle slider: Ja/Nej button switch with data-value attribute
* Clicking anywhere on the slider toggles the value
*/
private initToggleSliders(): void {
document.querySelectorAll('swp-toggle-slider').forEach(slider => {
slider.addEventListener('click', () => {
const el = slider as HTMLElement;
const newValue = el.dataset.value === 'yes' ? 'no' : 'yes';
el.dataset.value = newValue;
// Dispatch custom event for listeners
slider.dispatchEvent(new CustomEvent('toggle', {
bubbles: true,
detail: { value: newValue }
}));
});
});
}
/**
* Initialize all select dropdowns on the page
*/
private initSelectDropdowns(): void {
document.querySelectorAll('swp-select').forEach(select => {
const trigger = select.querySelector('button');
const dropdown = select.querySelector('swp-select-dropdown');
const options = select.querySelectorAll('swp-select-option');
if (!trigger || !dropdown) return;
// Toggle dropdown on button click
trigger.addEventListener('click', (e) => {
e.stopPropagation();
const isOpen = select.classList.toggle('open');
trigger.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
});
// Handle option selection
options.forEach(option => {
option.addEventListener('click', () => {
const value = (option as HTMLElement).dataset.value;
const label = option.textContent?.trim() || '';
// Update selected state
options.forEach(o => o.classList.remove('selected'));
option.classList.add('selected');
// Update trigger display
const valueEl = trigger.querySelector('swp-select-value');
if (valueEl) {
valueEl.textContent = label;
}
// Update color dot if present (for color pickers)
const triggerDot = trigger.querySelector('swp-color-dot');
if (triggerDot && value) {
triggerDot.className = `is-${value}`;
}
// Update data-value on select element
(select as HTMLElement).dataset.value = value;
// Close dropdown
select.classList.remove('open');
trigger.setAttribute('aria-expanded', 'false');
// Dispatch custom event
select.dispatchEvent(new CustomEvent('change', {
bubbles: true,
detail: { value, label }
}));
});
});
});
// Click outside to close any open dropdown
document.addEventListener('click', () => {
this.closeAllSelects();
});
// Keyboard navigation
document.addEventListener('keydown', (e) => {
const openSelect = document.querySelector('swp-select.open');
if (!openSelect) return;
// Escape to close
if (e.key === 'Escape') {
this.closeAllSelects();
return;
}
// Letter key to jump to option
if (e.key.length === 1 && /[a-zA-ZæøåÆØÅ]/.test(e.key)) {
const options = openSelect.querySelectorAll('swp-select-option');
const letter = e.key.toLowerCase();
for (const option of options) {
const text = option.textContent?.trim().toLowerCase() || '';
if (text.startsWith(letter)) {
// Scroll into view and highlight
option.scrollIntoView({ block: 'nearest' });
options.forEach(o => o.classList.remove('highlighted'));
option.classList.add('highlighted');
break;
}
}
}
// Enter to select highlighted option
if (e.key === 'Enter') {
const highlighted = openSelect.querySelector('swp-select-option.highlighted') as HTMLElement;
if (highlighted) {
highlighted.click();
}
}
});
}
private closeAllSelects(): void {
document.querySelectorAll('swp-select.open').forEach(select => {
select.classList.remove('open');
select.querySelector('button')?.setAttribute('aria-expanded', 'false');
});
}
}