Files
HMDesign/packages/org-chart/src/core/expanded-content.ts
T

110 lines
7.1 KiB
TypeScript

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<HTMLElement>('.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;
}