Newer
Older
gnexus-tasks / frontend / src / dropdownClamp.ts
// Дропдауны кита вне мыши: GnDropdown позиционирует меню от левого края триггера
// (position:absolute; left:0), и у триггеров у правого края экрана меню вылезает
// наружу. Кит не даёт ручек позиционирования — после открытия выравниваем меню
// (и по горизонтали, и по вертикали) в пределах вьюпорта, не трогая стили кита.
//
// На мобильных (до 767px, ТЗ 0.77) этого не нужно: меню целиком раскладывается
// как шторка снизу из kit-overrides.css, и инлайн-координаты только мешали бы.
const MOBILE = '(max-width: 767px)'

export function installDropdownClamp(): void {
  const clamp = () => {
    const mobile = window.matchMedia(MOBILE).matches
    for (const dropdown of Array.from(document.querySelectorAll<HTMLElement>('.dropdown.is-open'))) {
      const menu = dropdown.querySelector<HTMLElement>('.dropdown-menu')
      if (!menu) continue

      // Следы прошлого открытия (или прошлой ширины окна) снимаем всегда:
      // на мобильных они перебивали бы раскладку шторки
      menu.style.left = ''
      menu.style.right = ''
      menu.style.top = ''
      menu.style.bottom = ''
      if (mobile) continue

      const rect = dropdown.getBoundingClientRect()
      const vw = document.documentElement.clientWidth
      const vh = window.innerHeight

      // Горизонталь: если меню не помещается справа от триггера — открыть влево.
      // `left: auto` обязателен: у кита `.dropdown-menu{left:0}` — одного inline
      // `right: 0` мало, оба края оказываются заданными, и при min-width:220px
      // меню всё равно остаётся у левого края триггера (у правой колонки карточек
      // это выезд за вьюпорт на всю свою ширину, 0.79)
      if (rect.left + menu.offsetWidth > vw - 8) {
        menu.style.left = 'auto'
        menu.style.right = '0'
      } else {
        menu.style.left = '0'
      }

      // Вертикаль: если не помещается под триггером, но есть место сверху — открыть вверх
      if (rect.bottom + 8 + menu.offsetHeight > vh && rect.top - menu.offsetHeight > 0) {
        menu.style.top = 'auto'
        menu.style.bottom = 'calc(100% + 8px)'
      }
    }
  }
  // Открытие дропдауна происходит по клику синхронно, DOM обновляется в
  // микротаске Vue — macrotask setTimeout отрабатывает уже после рендера
  document.addEventListener('click', () => setTimeout(clamp, 0), true)
  // у открытого меню при смене ширины окна остаются координаты старого режима
  window.addEventListener('resize', clamp)
}