54451d2ea6
Bulk regions dialog reworked from federal-district bitmask to subject/region selection, consistent with ProjectDetailsDrawer/NewProjectDialog. Full-stack: add_regions/remove_regions on projects.regions INT[], BulkProjectActionRequest split validation, ProjectService model-instance update. federal-districts.ts removed (zero consumers). +menuRepositionFix util for v-autocomplete menu. phpstan-baseline: bump actingAs ignore count 14->15 (new validation test). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
53 lines
3.1 KiB
TypeScript
53 lines
3.1 KiB
TypeScript
/**
|
|
* Workaround для бага позиционирования Vuetify connected-location-strategy.
|
|
*
|
|
* Когда активатор `v-select`/`v-autocomplete` находится внутри
|
|
* `position: fixed`-контейнера (кастомный дровер, диалог), Vuetify включает
|
|
* ветку `activatorFixed` (`isFixedPosition()` → true). Её `getIntrinsicSize()`
|
|
* вычитает `el.style.left` из измеренной геометрии оверлея; на переходном
|
|
* кадре, когда контент ещё отрисован в нулевой позиции, а инлайновый
|
|
* `style.left` уже не нулевой, `contentBox.x` становится отрицательным и
|
|
* стратегия аккумулирует смещение — меню уезжает на кратное X активатора
|
|
* (за край экрана).
|
|
*
|
|
* Обычно гонку сглаживают пересчёты, размазанные по анимации открытия. Под
|
|
* `prefers-reduced-motion: reduce` (умолчание Windows Server) анимации нет —
|
|
* один пересчёт на «плохом» кадре остаётся финальным.
|
|
*
|
|
* Фикс: дождаться, пока контент оверлея отрисован и геометрически стабилен,
|
|
* затем один раз послать `resize` — Vuetify пересчитает позицию по уже
|
|
* устоявшейся геометрии и поставит меню корректно. Безопасно при motion ON
|
|
* (пересчёт по стабильной геометрии идемпотентен) и для не-fixed контейнеров.
|
|
*
|
|
* Привязывать к `@update:menu` нужного `v-autocomplete`/`v-select`.
|
|
*/
|
|
export function repositionMenuAfterOpen(open: boolean): void {
|
|
if (!open || typeof window === 'undefined') return;
|
|
|
|
let prevLeft = Number.NaN;
|
|
let stableFrames = 0;
|
|
let totalFrames = 0;
|
|
|
|
const tick = (): void => {
|
|
// Последний открытый overlay-menu (на случай вложенных оверлеев).
|
|
const menus = document.querySelectorAll<HTMLElement>('.v-overlay.v-menu .v-overlay__content');
|
|
const el = menus[menus.length - 1];
|
|
|
|
if (el && el.getBoundingClientRect().width > 0) {
|
|
const left = Math.round(el.getBoundingClientRect().left);
|
|
stableFrames = left === prevLeft ? stableFrames + 1 : 0;
|
|
prevLeft = left;
|
|
// 3 кадра без движения = геометрия устоялась → один чистый пересчёт.
|
|
if (stableFrames >= 3) {
|
|
window.dispatchEvent(new Event('resize'));
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Предохранитель ~1.5 c: не зацикливаться, если оверлей не появился.
|
|
if (++totalFrames < 90) requestAnimationFrame(tick);
|
|
};
|
|
|
|
requestAnimationFrame(tick);
|
|
}
|