SHA256
Обновить UI-ветку
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { renderHeader } from '../components/header.js';
|
||||
import { createOverflowDots } from '../components/overflow-dots.js';
|
||||
import { authService, state } from '../state.js';
|
||||
import { makeProfileRoute } from '../services/shine-routes.js';
|
||||
import { makeProfileLinksRoute } from '../services/shine-routes.js';
|
||||
@@ -493,18 +494,113 @@ export function render({ navigate, route, chrome } = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
let topMenuPortal = null;
|
||||
let topMenuBackdrop = null;
|
||||
let topMenuButton = null;
|
||||
|
||||
const closeTopMenu = () => {
|
||||
topMenuPortal?.remove();
|
||||
topMenuBackdrop?.remove();
|
||||
topMenuPortal = null;
|
||||
topMenuBackdrop = null;
|
||||
topMenuButton?.setAttribute('aria-expanded', 'false');
|
||||
topMenuButton?.classList.remove('menu-open-pressed');
|
||||
};
|
||||
|
||||
const positionTopMenu = () => {
|
||||
if (!topMenuPortal || !topMenuButton) return;
|
||||
const rect = topMenuButton.getBoundingClientRect();
|
||||
const margin = 10;
|
||||
const menuRect = topMenuPortal.getBoundingClientRect();
|
||||
const menuWidth = menuRect.width || topMenuPortal.offsetWidth || 190;
|
||||
const menuHeight = menuRect.height || topMenuPortal.offsetHeight || 56;
|
||||
const left = Math.max(margin, Math.min(window.innerWidth - menuWidth - margin, rect.right - menuWidth));
|
||||
const below = rect.bottom + 7;
|
||||
const top = below + menuHeight <= window.innerHeight - margin
|
||||
? below
|
||||
: Math.max(margin, rect.top - menuHeight - 7);
|
||||
topMenuPortal.style.left = `${Math.round(left)}px`;
|
||||
topMenuPortal.style.top = `${Math.round(top)}px`;
|
||||
};
|
||||
|
||||
const openTopMenu = () => {
|
||||
if (!topMenuButton || topMenuPortal) return;
|
||||
const portal = document.createElement('div');
|
||||
portal.className = 'dm-head-menu dm-head-menu--portal network-head-menu';
|
||||
portal.setAttribute('role', 'menu');
|
||||
portal.innerHTML = `
|
||||
<button type="button" class="dm-head-menu-item network-head-menu-item" role="menuitem" data-action="find-person">
|
||||
<span>Найти человека</span>
|
||||
</button>
|
||||
`;
|
||||
portal.querySelector('[data-action="find-person"]')?.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
closeTopMenu();
|
||||
openSearchModal();
|
||||
});
|
||||
portal.addEventListener('click', (event) => event.stopPropagation());
|
||||
const backdrop = document.createElement('div');
|
||||
backdrop.className = 'ui-menu-dim-layer';
|
||||
backdrop.addEventListener('click', closeTopMenu);
|
||||
document.body.append(backdrop, portal);
|
||||
topMenuBackdrop = backdrop;
|
||||
topMenuPortal = portal;
|
||||
topMenuButton.setAttribute('aria-expanded', 'true');
|
||||
topMenuButton.classList.add('menu-open-pressed');
|
||||
positionTopMenu();
|
||||
requestAnimationFrame(positionTopMenu);
|
||||
};
|
||||
|
||||
const toggleTopMenu = (event) => {
|
||||
event?.preventDefault?.();
|
||||
event?.stopPropagation?.();
|
||||
if (topMenuPortal) closeTopMenu();
|
||||
else openTopMenu();
|
||||
};
|
||||
|
||||
const header = renderHeader({
|
||||
title: 'Связи',
|
||||
rightActions: [
|
||||
{ label: 'Найти', onClick: openSearchModal },
|
||||
{
|
||||
iconNode: createOverflowDots(),
|
||||
className: 'network-top-more-btn',
|
||||
title: 'Ещё действия',
|
||||
ariaLabel: 'Меню связей',
|
||||
onClick: toggleTopMenu,
|
||||
},
|
||||
],
|
||||
});
|
||||
// «Связи» используют тот же общий topbar, что и остальные страницы.
|
||||
// Отдельный класс нужен только для page-specific fade графа, не для геометрии header.
|
||||
header.classList.add('network-topbar');
|
||||
topMenuButton = header.querySelector('.network-top-more-btn');
|
||||
topMenuButton?.setAttribute('aria-haspopup', 'menu');
|
||||
topMenuButton?.setAttribute('aria-expanded', 'false');
|
||||
|
||||
const onTopMenuOutsideClick = (event) => {
|
||||
if (!topMenuPortal) return;
|
||||
if (topMenuPortal.contains(event.target) || topMenuButton?.contains(event.target)) return;
|
||||
closeTopMenu();
|
||||
};
|
||||
const onTopMenuKeydown = (event) => {
|
||||
if (event.key !== 'Escape' || !topMenuPortal) return;
|
||||
closeTopMenu();
|
||||
topMenuButton?.focus();
|
||||
};
|
||||
const onTopMenuViewportChange = () => positionTopMenu();
|
||||
document.addEventListener('click', onTopMenuOutsideClick);
|
||||
document.addEventListener('keydown', onTopMenuKeydown);
|
||||
window.addEventListener('resize', onTopMenuViewportChange, { passive: true });
|
||||
window.addEventListener('scroll', onTopMenuViewportChange, { passive: true, capture: true });
|
||||
|
||||
// Ресайз и перерисовку рёбер движок обрабатывает сам (window resize + ResizeObserver внутри).
|
||||
screen.cleanup = () => {
|
||||
closeTopMenu();
|
||||
document.removeEventListener('click', onTopMenuOutsideClick);
|
||||
document.removeEventListener('keydown', onTopMenuKeydown);
|
||||
window.removeEventListener('resize', onTopMenuViewportChange);
|
||||
window.removeEventListener('scroll', onTopMenuViewportChange, true);
|
||||
if (engine) engine.destroy();
|
||||
engine = null;
|
||||
appScreenEl?.classList.remove('network-scroll-lock');
|
||||
|
||||
Reference in New Issue
Block a user