feat(org-chart): add core cards and fixed-layout integration
This commit is contained in:
1 parent
d18e3c3380
commit
61c6f8d1f1
161 files changed
+13044
-1
No files matched your search
@@ -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();
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user