import { createEmployeeCard } from './employee-card.js'; import { createHeading, type RenderContext } from './render.js'; import type { RenderEngineNode } from './engine.js'; import { getCompanyColorPreset } from '../company-color-presets.js'; import { alignExpandedLevels } from './expanded-layout.js'; import type { OrgChartWheelZoom } from './types.js'; /** Auto-only presenter. The source and popup identity remain unchanged. */ export function decorateExpandedNode(node: HTMLElement, value: RenderEngineNode, context: RenderContext, signal: AbortSignal): void { const organization = context.orderedOrganizations[Number(value.organizationIndex)]!; const companyId = context.organizationCompanies!.get(organization.id)!; const preset = getCompanyColorPreset(companyId)!; const document = node.ownerDocument; const article = node.querySelector('.hm-orgchart-organization')!; const collapsed = context.collapsedOrganizations!.has(organization.id); const top = organization.parentId === null ? context.companyTopNodes?.[companyId] : undefined; node.dataset.organizationId = organization.id; node.dataset.parentId = organization.parentId ?? ''; node.dataset.depth = node.dataset.organizationDepth = String(value.depth); node.dataset.organizationType = organization.type; node.dataset.companyId = companyId; node.dataset.collapsed = String(collapsed); node.draggable = false; const counts = context.summary.raw.byOrganization[organization.id]; node.dataset.directEmployeeCount = String(counts?.direct ?? 0); node.dataset.inclusiveEmployeeCount = String(counts?.inclusive ?? 0); const vertical = value.vertical === true || (context.autoLayout?.type === 'hybrid' && Number(value.depth) + 1 >= context.autoLayout.hybridStartDepth); node.dataset.verticalLayout = String(vertical); const layoutDirection = context.autoLayout?.type === 'lr' || vertical ? 'left' : 'top'; for (const edge of node.querySelectorAll('.edge')) edge.setAttribute('aria-hidden', 'true'); const leaderId = top?.employeeId === undefined ? organization.leaderId : top.employeeId; const leader = context.data.employees.find(employee => employee.id === leaderId && employee.organizationId === organization.id); const staff = context.data.presentation?.groups.showEmployees === false ? [] : (context.employeesByOrganization.get(organization.id) ?? []).filter(employee => employee.id !== leaderId); const hasChildren = context.data.organizations.some(org => org.parentId === organization.id) || staff.length > 0; const heading = document.createElement('h3'); heading.className = 'hm-orgchart-organization-name'; const button = document.createElement('button'); button.type = 'button'; button.className = 'hm-orgchart-expanded-toggle'; button.dataset.organizationId = organization.id; button.textContent = `${top?.organizationName ?? organization.name} ${hasChildren ? collapsed ? '▸' : '▾' : '·'}`; button.setAttribute('aria-label', `${top?.organizationName ?? organization.name} 하위 조직·직원 · 직접 ${counts?.direct ?? 0}명 · 하위 포함 ${counts?.inclusive ?? 0}명`); button.setAttribute('aria-expanded', String(!collapsed)); // Empty titles remain focusable move sources/targets. Collapse is independent. button.setAttribute('aria-disabled', String(!hasChildren || context.interactions?.collapse === false)); button.addEventListener('click', event => { event.stopPropagation(); if (hasChildren && context.interactions?.collapse !== false) context.toggleExpanded?.(organization.id); }, { signal }); heading.append(button); const href = context.organizationLinks?.[organization.id]; if (href) { const link = document.createElement('a'); link.className = 'hm-orgchart-expanded-link'; link.href = href; link.textContent = '↗'; link.setAttribute('aria-label', `${organization.name} 페이지 이동`); link.addEventListener('click', event => event.stopPropagation(), { signal }); heading.append(link); } article.append(heading); const colorCard = (card: HTMLElement, affiliationId: string) => { card.draggable = false; const employeePreset = getCompanyColorPreset(affiliationId) ?? preset; card.dataset.companyId = employeePreset.companyId; return card; }; if (leader && top?.display !== 'hidden' && top?.display !== 'titleOnly') { const presentation = top ? { ...leader, position: top.title ?? leader.position, name: top.name ?? leader.name, profileImage: top.profileImage === undefined ? leader.profileImage : top.profileImage } : leader; const card = colorCard(createEmployeeCard(document, presentation, { kind: organization.parentId === null ? 'topLeader' : 'leader', expandedPresentation: true, hideStatusLabel: true }, signal), leader.affiliationId); card.dataset.connectionDirection = context.autoLayout?.type === 'lr' ? 'left' : 'top'; article.append(card); } else if (leader && top?.display === 'titleOnly') { const title = document.createElement('p'); title.className = 'hm-orgchart-top-node-title'; title.textContent = `${top.title ?? leader.position} · ${top.name ?? leader.name}`; article.append(title); } if (!collapsed && staff.length) { const employees = document.createElement('div'); employees.className = 'hm-orgchart-employees'; const direction = context.employeeConnections?.[organization.id] ?? layoutDirection; employees.dataset.connectionDirection = direction; for (const employee of staff) { const card = colorCard(createEmployeeCard(document, employee, { kind: 'employee', expandedPresentation: true, hideStatusLabel: true }, signal), employee.affiliationId); card.dataset.connectionDirection = direction; employees.append(card); } article.append(employees); } } export function createExpandedOrgChartContent(document: Document, context: RenderContext, tree: HTMLElement, signal: AbortSignal, layoutReady: () => void, presentation: { chartOnly?: boolean; wheelZoom?: OrgChartWheelZoom } = {}): HTMLElement { const content = document.createElement('div'); content.className = 'hm-orgchart-expanded'; content.dataset.component = 'ExpandedOrgChartContent'; content.dataset.layoutMode = 'auto'; if (presentation.chartOnly) content.dataset.presentationMode = 'chart-only'; content.dataset.autoLayout = context.autoLayout?.type ?? 'tb'; content.dataset.hybridStartDepth = String(context.autoLayout?.hybridStartDepth ?? 3); content.dataset.initialVisibleDepth = String(context.autoLayout?.initialVisibleDepth ?? 'all'); content.dataset.groupHybrid = String(!!context.autoLayout?.hybridOrganizationIds.length); content.dataset.layoutStatus = 'hierarchical'; content.dataset.cardDesignStatus = 'current-presentation'; tree.setAttribute('aria-label', `확장형 조직 계층도 · ${presentation.wheelZoom === 'direct' ? '휠로 확대·축소' : 'Ctrl 또는 Command + 휠로 확대'} · 드래그로 이동`); // Chart Only: titles inside organization nodes remain; the outer heading does not. if (presentation.chartOnly) content.append(tree); else content.append(createHeading(document, context), tree); alignExpandedLevels(tree, signal, layoutReady, context.autoLayout); return content; }