// Общее контекстное меню узла (долгое нажатие) для карты связей. // Рендерится в #modal-root (вне масштабируемого холста), позиционируется по экранному rect узла. // Используется и реальным путём (network-view.js), и лабораторией (lab.js). const REL_RU = { family: 'Семья', friend: 'Друг', business: 'Бизнес', contact: 'Контакт', self: 'Вы', }; function escapeHtml(text) { return String(text || '') .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } export function relationLabelRu(relationType) { return REL_RU[relationType] || 'Контакт'; } /** * Открывает контекстное меню узла. * @param {Object} opts * @param {string} opts.login - логин/идентификатор для заголовка * @param {string} opts.relationType - тип связи (для подписи) * @param {{x:number,y:number,rect?:DOMRect}} opts.point - экранная точка/rect узла * @param {Array<{label:string, onClick:Function, disabled?:boolean}>} opts.actions - пункты меню */ export function openNodeMenu({ login, relationType, point, actions = [] } = {}) { const root = document.getElementById('modal-root'); if (!(root instanceof HTMLElement)) return; const itemsHtml = actions .map((a, i) => ``) .join(''); root.innerHTML = `
`; const overlay = root.querySelector('#fg-menu-overlay'); const menu = root.querySelector('#fg-menu'); if (!(overlay instanceof HTMLElement) || !(menu instanceof HTMLElement)) { root.innerHTML = ''; return; } // позиционируем рядом с узлом по его экранному rect (синхронно, без rAF — он может троттлиться) const rect = point?.rect; const px = point?.x ?? (rect ? rect.left + rect.width / 2 : window.innerWidth / 2); const belowY = rect ? rect.bottom + 8 : (point?.y ?? 80); const mw = menu.offsetWidth; const mh = menu.offsetHeight; let left = px - mw / 2; left = Math.max(8, Math.min(left, window.innerWidth - mw - 8)); let top = belowY; if (top + mh > window.innerHeight - 8) top = Math.max(8, (rect ? rect.top : belowY) - mh - 8); menu.style.left = `${left}px`; menu.style.top = `${top}px`; const close = () => { root.innerHTML = ''; }; overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); menu.addEventListener('click', (e) => { const btn = e.target instanceof HTMLElement ? e.target.closest('[data-i]') : null; if (!(btn instanceof HTMLElement)) return; const idx = Number(btn.dataset.i); const action = actions[idx]; if (!action || action.disabled) return; close(); if (typeof action.onClick === 'function') action.onClick(); }); return close; }