feat(org-chart): add core cards and fixed-layout integration

This commit is contained in:
shkim4 committed 2026-10-12 17:58:30 +09:00
1 parent d18e3c3380
commit 61c6f8d1f1
161 files changed
+13044 -1

No files matched your search

+143
View File
@@ -0,0 +1,143 @@
import type OrgChart from 'orgchart';
import type { EngineNode } from 'orgchart';
export type RenderEngineNode = EngineNode;
export interface EngineAdapter {
update(data: RenderEngineNode[]): void;
toggle(node: HTMLElement): boolean | undefined;
dispose(): void;
}
/** Importing the public facade never evaluates the upstream browser-only module. */
export async function loadEngine() {
const { default: Upstream } = await import('orgchart');
// 6.0.0 has no destroy(). Its init observer is local to triggerInitEvent.
// Override that narrow hook on our subclass to keep the resource disposable.
return class ManagedOrgChart extends Upstream {
declare initObserver?: MutationObserver;
override init(options?: Parameters<OrgChart['init']>[0]): OrgChart {
try {
return super.init(options);
} catch (error) {
this.disconnectInitObserver();
this.chart?.remove();
this.chart = null;
throw error;
}
}
override triggerInitEvent(): void {
this.disconnectInitObserver();
const Observer = this.chartContainer?.ownerDocument.defaultView?.MutationObserver;
if (!Observer || !this.chartContainer) return;
const observer = new Observer(() => {
observer.disconnect();
if (this.initObserver === observer) this.initObserver = undefined;
if (this.chart) this.handleCompactNodes();
});
this.initObserver = observer;
observer.observe(this.chartContainer, { childList: true });
}
disconnectInitObserver(): void {
this.initObserver?.disconnect();
this.initObserver = undefined;
}
};
}
export type EngineConstructor = Awaited<ReturnType<typeof loadEngine>>;
export function createEngineAdapter(
Engine: EngineConstructor,
viewport: HTMLElement,
data: RenderEngineNode[],
decorate: (node: HTMLElement, data: RenderEngineNode, signal: AbortSignal) => void,
): EngineAdapter {
const Abort = viewport.ownerDocument.defaultView?.AbortController ?? AbortController;
let generation = new Abort();
let instances: (OrgChart & { disconnectInitObserver(): void })[] = [];
let disposed = false;
const forest = viewport.ownerDocument.createElement('div');
forest.className = 'hm-orgchart-forest';
viewport.append(forest);
function releaseGeneration(): void {
generation.abort();
for (const instance of instances) {
instance.disconnectInitObserver();
// Keep the old chart reference until upstream feature cleanup is complete.
instance.unbindPan(); instance.unbindZoom(); instance.detachMinimap();
}
}
function disposeInstances(): void {
releaseGeneration();
for (const instance of instances) {
instance.chart?.remove();
instance.chart = null; instance.chartContainer = null;
instance.opts = {}; instance.options = {};
}
instances = [];
forest.replaceChildren();
}
function build(roots: RenderEngineNode[]): void {
try {
for (const root of roots) {
const treeContainer = viewport.ownerDocument.createElement('div');
treeContainer.className = 'hm-orgchart-tree';
forest.append(treeContainer);
const instance = new Engine(treeContainer, {
data: root, pan: false, zoom: false, minimap: false, draggable: false, exportButton: false,
chartClass: 'hm-orgchart-engine',
nodeTemplate: () => '<article class="hm-orgchart-organization"></article>',
createNode: (node, value) => decorate(node, value, generation.signal),
});
instances.push(instance);
}
} catch (error) { disposeInstances(); throw error; }
}
function syncButtons(): void {
for (const instance of instances) {
for (const node of instance.chart?.querySelectorAll<HTMLElement>('.node') ?? []) {
const button = node.querySelector<HTMLButtonElement>('.hm-orgchart-toggle');
if (!button) continue;
const expanded = instance.getNodeState(node, 'children').visible && !node.closest('.hierarchy')?.classList.contains('isChildrenCollapsed');
button.setAttribute('aria-expanded', String(expanded));
const label = expanded ? '하위 조직 접기' : '하위 조직 펼치기';
button.textContent = label;
button.setAttribute('aria-label', `${node.querySelector('.hm-orgchart-organization-name')?.textContent ?? ''} ${label}`);
}
}
}
function bindGeneration(): void {
forest.addEventListener('transitionend', syncButtons, { signal: generation.signal });
}
build(data);
bindGeneration();
return {
update(next) {
if (disposed) throw new Error('OrgChart engine is disposed');
releaseGeneration();
generation = new Abort();
if (next.length === instances.length) {
for (let index = 0; index < next.length; index++) instances[index].init({ data: next[index] });
} else {
disposeInstances();
generation = new Abort();
build(next);
}
bindGeneration();
},
toggle(node) {
const instance = instances.find(candidate => candidate.chart?.contains(node));
if (!instance || node.closest('.hierarchy')?.querySelector('.sliding')) return undefined;
const expanded = instance.getNodeState(node, 'children').visible;
if (expanded) instance.hideChildren(node); else instance.showChildren(node);
syncButtons();
return !expanded;
},
dispose() {
if (disposed) return;
disposed = true;
disposeInstances();
viewport.replaceChildren();
},
};
}