mapdash/frontend/src/dropdown.ts
aaverbitskiy aeb4651f6f polish: appearance menu + clean status names everywhere (v0.2.1)
Appearance ("Оформление") menu:
- add missing status "РК. Предварительное бронирование" (8 statuses total)
- strip the systemic "РК. " prefix from status names in the UI (menu,
  tooltip, status filter dropdown, active-filter chips); raw values stay
  intact for filtering/URLs
- fixed-position menu: opens 25px below the header, ends >=30px above the
  window bottom, scrolls inside — never overflows the viewport
- drop the "Прочие статусы" row
- per-status grey reset glyph (circular arrows) restoring its default colour

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 15:37:03 +00:00

132 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Reusable checkbox dropdown filter (multi-select) — ported from the original
// createDropdown(). Expects markup: #<id>-dropdown, #<id>-btn, #<id>-btn-text,
// #<id>-panel already present in the DOM.
export interface Dropdown {
setValues(values: string[], label: string): void;
/** Replace the available options while keeping the current selection. */
updateValues(values: string[]): void;
getSelected(): string[];
/** Replace the current selection (does NOT fire onChange — caller reloads). */
setSelected(values: string[]): void;
/** Clear the selection (does NOT fire onChange — caller reloads). */
clear(): void;
onChange(fn: () => void): void;
}
// `formatLabel` maps a raw value to its display text (values stay raw for
// filtering/URLs); defaults to identity.
export function createDropdown(id: string, formatLabel: (v: string) => string = (v) => v): Dropdown {
const root = document.getElementById(id + '-dropdown') as HTMLElement;
const btn = document.getElementById(id + '-btn') as HTMLElement;
const btnText = document.getElementById(id + '-btn-text') as HTMLElement;
const panel = document.getElementById(id + '-panel') as HTMLElement;
let allValues: string[] = [];
let selected = new Set<string>();
let defaultLabel = '';
let onChange: () => void = () => {};
function updateBtnText(): void {
if (selected.size === 0) btnText.textContent = defaultLabel;
else if (selected.size === allValues.length) btnText.textContent = 'Все (' + allValues.length + ')';
else if (selected.size <= 2) btnText.textContent = Array.from(selected).map(formatLabel).join(', ');
else btnText.textContent = 'Выбрано: ' + selected.size;
}
function syncCheckboxes(): void {
panel.querySelectorAll<HTMLInputElement>('input[type=checkbox]').forEach((cb) => {
cb.checked = selected.has(cb.value);
});
}
function renderPanel(): void {
panel.innerHTML = '';
const actions = document.createElement('div');
actions.className = 'dp-actions';
const selAll = document.createElement('a');
selAll.textContent = 'Выбрать все';
selAll.addEventListener('click', () => {
selected = new Set(allValues);
syncCheckboxes();
updateBtnText();
onChange();
});
const selNone = document.createElement('a');
selNone.textContent = 'Сбросить';
selNone.addEventListener('click', () => {
selected.clear();
syncCheckboxes();
updateBtnText();
onChange();
});
actions.appendChild(selAll);
actions.appendChild(selNone);
panel.appendChild(actions);
// Show all available options, plus any currently-selected value that is no
// longer among them (so a selection made incompatible by other filters
// stays visible and can still be unchecked).
const extra = Array.from(selected).filter((v) => !allValues.includes(v));
const display = allValues.concat(extra);
for (const v of display) {
const label = document.createElement('label');
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.value = v;
cb.checked = selected.has(v);
cb.addEventListener('change', () => {
if (cb.checked) selected.add(v);
else selected.delete(v);
updateBtnText();
onChange();
});
label.appendChild(cb);
label.appendChild(document.createTextNode(formatLabel(v)));
panel.appendChild(label);
}
}
btn.addEventListener('click', (e) => {
e.stopPropagation();
document.querySelectorAll('.dropdown.open').forEach((d) => {
if (d !== root) d.classList.remove('open');
});
root.classList.toggle('open');
});
document.addEventListener('click', (e) => {
if (!root.contains(e.target as Node)) root.classList.remove('open');
});
return {
setValues(values: string[], label: string): void {
allValues = values;
defaultLabel = label;
selected = new Set();
updateBtnText();
renderPanel();
},
updateValues(values: string[]): void {
allValues = values;
updateBtnText();
renderPanel();
},
getSelected(): string[] {
return Array.from(selected);
},
setSelected(values: string[]): void {
selected = new Set(values);
syncCheckboxes();
updateBtnText();
},
clear(): void {
selected.clear();
syncCheckboxes();
updateBtnText();
},
onChange(fn: () => void): void {
onChange = fn;
},
};
}