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>
132 lines
4.4 KiB
TypeScript
132 lines
4.4 KiB
TypeScript
// 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;
|
||
},
|
||
};
|
||
}
|