feat(org-chart): finalize chart features and AI generation rules

This commit is contained in:
shkim4 committed 2026-10-13 14:39:42 +09:00
1 parent 7614507583
commit d0828a7ff4
80 files changed
+8152 -221

No files matched your search

+53 -1
View File
@@ -1,10 +1,17 @@
import type OrgChart from 'orgchart';
import type { EngineNode } from 'orgchart';
import type { EngineNode, EngineOptions } from 'orgchart';
export type RenderEngineNode = EngineNode;
export interface EngineEditing {
canDrop(source: HTMLElement, target: HTMLElement): boolean;
onDrop(source: HTMLElement, target: HTMLElement): void;
}
export interface EngineAdapter {
update(data: RenderEngineNode[]): void;
toggle(node: HTMLElement): boolean | undefined;
prepareDrag(source: HTMLElement): void;
drop(source: HTMLElement, target: HTMLElement): void;
cancelDrag(): void;
dispose(): void;
}
@@ -51,6 +58,8 @@ export function createEngineAdapter(
viewport: HTMLElement,
data: RenderEngineNode[],
decorate: (node: HTMLElement, data: RenderEngineNode, signal: AbortSignal) => void,
layoutOptions: Pick<EngineOptions, 'direction' | 'verticalLevel' | 'visibleLevel'> = {},
editing?: EngineEditing,
): EngineAdapter {
const Abort = viewport.ownerDocument.defaultView?.AbortController ?? AbortController;
let generation = new Abort();
@@ -59,7 +68,12 @@ export function createEngineAdapter(
const forest = viewport.ownerDocument.createElement('div');
forest.className = 'hm-orgchart-forest';
viewport.append(forest);
function cancelDrag(): void {
if (!editing) return;
for (const instance of instances) instance.dragendHandler();
}
function releaseGeneration(): void {
cancelDrag();
generation.abort();
for (const instance of instances) {
instance.disconnectInitObserver();
@@ -84,8 +98,10 @@ export function createEngineAdapter(
treeContainer.className = 'hm-orgchart-tree';
forest.append(treeContainer);
const instance = new Engine(treeContainer, {
...layoutOptions,
data: root, pan: false, zoom: false, minimap: false, draggable: false, exportButton: false,
chartClass: 'hm-orgchart-engine',
...(editing ? { dropCriteria: (source: HTMLElement, _parent: HTMLElement | null, target: HTMLElement) => editing.canDrop(source, target) } : {}),
nodeTemplate: () => '<article class="hm-orgchart-organization"></article>',
createNode: (node, value) => decorate(node, value, generation.signal),
});
@@ -108,6 +124,23 @@ export function createEngineAdapter(
}
function bindGeneration(): void {
forest.addEventListener('transitionend', syncButtons, { signal: generation.signal });
if (!editing) return;
for (const instance of instances) {
// Core owns immutable data and rerendering. Reuse upstream drop events,
// but cancel its DOM/data mutation before either horizontal or hybrid path.
const commitDrop = (event: Event) => {
event.preventDefault();
const detail = (event as CustomEvent<{ draggedNode?: HTMLElement; draggedItem?: HTMLElement; dropZone: HTMLElement }>).detail;
if (!detail) return;
const source = detail.draggedNode ?? detail.draggedItem;
const target = detail.dropZone;
if (source && target && instance.chart?.contains(source) && instances.some(candidate => candidate.chart?.contains(target)) && editing.canDrop(source, target)) {
editing.onDrop(source, target);
}
};
instance.chart?.addEventListener('nodedrop.orgchart', commitDrop, { signal: generation.signal });
instance.chart?.addEventListener('otherdropped.orgchart', commitDrop, { signal: generation.signal });
}
}
build(data);
bindGeneration();
@@ -133,6 +166,25 @@ export function createEngineAdapter(
syncButtons();
return !expanded;
},
prepareDrag(source) {
if (disposed || !editing) return;
cancelDrag();
if (!instances.some(candidate => candidate.chart?.contains(source))) return;
// Independent company trees share one editing policy. Upstream caches the
// source and filters targets within its own chart, so filter each tree.
for (const instance of instances) instance.filterAllowedDropNodes(source);
},
drop(source, target) {
if (disposed || !editing) return;
const instance = instances.find(candidate => candidate.chart?.contains(source));
if (!instance || !instances.some(candidate => candidate.chart?.contains(target)) || !editing.canDrop(source, target)) { cancelDrag(); return; }
// Refresh upstream's cached source and allowed targets for the final data
// policy check. Pointer coordinates and thresholds stay in the Core UI.
for (const candidate of instances) candidate.filterAllowedDropNodes(source);
void instance.dropHandler({ dropZone: target });
cancelDrag();
},
cancelDrag,
dispose() {
if (disposed) return;
disposed = true;