@yworks/react-yfiles-orgchart
Version:
yFiles React Organization Chart Component - A powerful and versatile React component based on the yFiles library, allows you to seamlessly incorporate dynamic and interactive organization charts into your applications.
1,433 lines (1,416 loc) • 54 kB
JavaScript
// src/OrgChartProvider.tsx
import { createContext, useContext, useMemo } from "react";
// src/core/CollapsibleTree.ts
import {
BaseClass,
Command,
CompactSubtreePlacer,
CompositeLayoutData,
delegate,
FilteredGraphWrapper,
Graph,
GraphStructureAnalyzer,
HierarchicalLayout,
HierarchicalLayoutData,
HierarchicalLayoutEdgeDescriptor,
IEdge,
INode,
ITreeLayoutPortAssigner,
LayoutAnchoringPolicy,
LayoutAnchoringStage,
LayoutAnchoringStageData,
LayoutExecutor,
Mapper,
MutableRectangle,
PlaceNodesAtBarycenterStage,
PlaceNodesAtBarycenterStageData,
Point,
PortPlacementPolicy,
Rect,
TreeLayout,
TreeLayoutData,
TreeReductionStage,
ViewportLimitingPolicy
} from "@yfiles/yfiles";
var CollapsibleTree = class _CollapsibleTree {
constructor(_graphComponent, completeGraph = new Graph()) {
this._graphComponent = _graphComponent;
this.completeGraph = completeGraph;
const nodeFilter = (node) => !this.hiddenNodesSet.has(node);
this.filteredGraph = new FilteredGraphWrapper(completeGraph, nodeFilter);
_graphComponent.viewportLimiter.policy = ViewportLimitingPolicy.WITHIN_MARGINS;
_graphComponent.maximumZoom = 4;
_graphComponent.minimumZoom = 0.1;
}
hiddenNodesSet = /* @__PURE__ */ new Set();
filteredGraph;
doingLayout = false;
// once the nodes have been arranged, remember their arrangement strategy for a more stable layout upon changes
compactSubtreePlacerStrategyMementos = new Mapper();
graphUpdatedListener = null;
collapsedStateUpdatedListener = null;
/**
* Optional predicate that determines whether a node is an assistant. This affects the
* placement of a node. See also {@link TreeLayoutData#assistantNodes}.
*/
isAssistantNode = () => false;
/**
* Optional mapping of a node to its type affecting the order of nodes in the layout.
* See also {@link TreeLayoutData.nodeTypes}.
*/
nodeTypesMapping = () => null;
/**
* Optional comparer to determine the order of subtrees in the layout.
* See also {@link TreeLayoutData.childOrder.outEdgeComparison}.
*/
outEdgeComparison = () => () => 0;
get graphComponent() {
return this._graphComponent;
}
/**
* Adds an event listener to the graphUpdated event that is fired after the filtered graph
* has changed and the layout was updated.
*/
addGraphUpdatedListener(listener) {
this.graphUpdatedListener = delegate.combine(this.graphUpdatedListener, listener);
}
removeGraphUpdatedListener(listener) {
this.graphUpdatedListener = delegate.remove(this.graphUpdatedListener, listener);
}
/**
* Adds an event listener to the collapsedStateUpdated event that is fired when the collapsed
* state of a port has changed.
*/
addCollapsedStateUpdatedListener(listener) {
this.collapsedStateUpdatedListener = delegate.combine(
this.collapsedStateUpdatedListener,
listener
);
}
removeCollapsedStateUpdatedListener(listener) {
this.collapsedStateUpdatedListener = delegate.remove(
this.collapsedStateUpdatedListener,
listener
);
}
/**
* Hides the children of the given node and updates the layout.
*/
async executeHideChildren(item) {
if (!this.canExecuteHideChildren(item)) {
return Promise.resolve();
}
const descendants = _CollapsibleTree.collectDescendants(this.completeGraph, item);
for (const node of descendants) {
this.updateCollapsedState(node, true);
}
this.updateCollapsedState(item, true);
const allSiblingNodes = this.completeGraph.outEdgesAt(item).map((outEdge) => outEdge.targetNode).flatMap((targetNode) => this.completeGraph.inEdgesAt(targetNode)).map((edge) => edge.sourceNode).filter((sourceNode) => sourceNode !== item).distinct();
allSiblingNodes.forEach((node) => {
if (this.filteredGraph.outEdgesAt(node).some((edge) => descendants.has(edge.targetNode))) {
this.updateCollapsedState(node, true);
}
});
this.removeEmptyGroups(descendants);
this.filteredGraph.nodePredicateChanged();
await this.refreshLayout(item, descendants, true);
this.addToHiddenNodes(descendants);
this.filteredGraph.nodePredicateChanged();
this.onGraphUpdated();
}
/**
* @returns Whether the children of the given node can be hidden.
*/
canExecuteHideChildren(item) {
return !this.doingLayout && this.filteredGraph.outDegree(item) > 0;
}
/**
* Shows the children of the given node and updates the layout.
*/
async executeShowChildren(item) {
if (!this.canExecuteShowChildren(item)) {
return Promise.resolve();
}
const descendants = _CollapsibleTree.collectDescendants(this.completeGraph, item);
const incrementalNodes = new Set(
Array.from(descendants).filter((node) => !this.filteredGraph.contains(node))
);
this.showChildren(item);
this.filteredGraph.nodePredicateChanged();
await this.refreshLayout(item, incrementalNodes, false);
this.updateCollapsedState(item, false);
const allSiblingNodes = this.completeGraph.outEdgesAt(item).map((outEdge) => outEdge.targetNode).flatMap((targetNode) => this.completeGraph.inEdgesAt(targetNode)).filter((edge) => edge.sourceNode !== item).map((edge) => edge.sourceNode).distinct();
allSiblingNodes.forEach((node) => {
if (this.completeGraph.outEdgesAt(node).every((edge) => descendants.has(edge.targetNode))) {
this.updateCollapsedState(node, false);
}
});
this.onGraphUpdated();
}
showChildren(node) {
for (const childEdge of this.completeGraph.outEdgesAt(node)) {
const child = childEdge.targetNode;
this.hiddenNodesSet.delete(child);
_CollapsibleTree.restoreGroup(this.completeGraph, this.hiddenNodesSet, child);
this.onCollapsedStateUpdated(childEdge.sourcePort, false);
}
}
/**
* @returns Whether the children of the given node can be shown.
*/
canExecuteShowChildren(item) {
return !this.doingLayout && this.filteredGraph.outDegree(item) !== this.completeGraph.outDegree(item);
}
/**
* Shows the parent of the given node and updates the layout.
*
* In contrast to {@link executeHideParent}, it only shows the
* direct parent and not any of its children.
*/
async executeShowParent(node) {
if (this.doingLayout) {
return Promise.resolve();
}
const incrementalNodes = /* @__PURE__ */ new Set();
this.showParents(node, incrementalNodes);
this.filteredGraph.nodePredicateChanged();
await this.refreshLayout(node, incrementalNodes, false);
this.onGraphUpdated();
}
showParents(node, incrementalNodes) {
for (const parentEdge of this.completeGraph.inEdgesAt(node)) {
const parent = parentEdge.sourceNode;
this.hiddenNodesSet.delete(parent);
_CollapsibleTree.restoreGroup(this.completeGraph, this.hiddenNodesSet, parent);
incrementalNodes.add(parent);
}
}
/**
* @returns Whether the parent of the given node can be shown.
*/
canExecuteShowParent(node) {
return !this.doingLayout && this.filteredGraph.inDegree(node) === 0 && this.completeGraph.inDegree(node) > 0;
}
/**
* Hides the parent of the given node and updates the layout.
*
* In contrast to {@link executeShowParent}, this method also hides all ancestors
* and their descendants and other isolated trees leaving only the node and its descendants
* in the graph.
*/
async executeHideParent(node) {
if (this.doingLayout) {
return Promise.resolve();
}
const nodes = _CollapsibleTree.collectAllNodesExceptSubtree(this.completeGraph, node);
this.removeEmptyGroups(nodes);
this.filteredGraph.nodePredicateChanged();
await this.refreshLayout(node, nodes, true);
this.addToHiddenNodes(nodes);
this.filteredGraph.nodePredicateChanged();
this.onGraphUpdated();
}
/**
* @returns Whether the parent of the given node can be hidden.
*/
canExecuteHideParent(node) {
return !this.doingLayout && this.filteredGraph.inDegree(node) > 0;
}
/**
* Shows all nodes and updates the layout.
*/
async executeShowAll() {
if (this.doingLayout) {
return Promise.resolve();
}
const incrementalNodes = new Set(this.hiddenNodesSet);
this.hiddenNodesSet.clear();
for (const edge of this.completeGraph.edges) {
this.onCollapsedStateUpdated(edge.sourcePort, false);
}
this.filteredGraph.nodePredicateChanged();
await this.refreshLayout(
this._graphComponent.currentItem,
incrementalNodes,
false
);
this.onGraphUpdated();
}
/**
* @returns Whether {@link executeShowAll} can be executed.
*/
canExecuteShowAll() {
return this.hiddenNodesSet.size !== 0 && !this.doingLayout;
}
/**
* Applies the initial layout to the graph.
*/
applyInitialLayout(fromSketch = false, incrementalNodes = []) {
if (this.doingLayout) {
return;
}
if (!fromSketch) {
this.hiddenNodesSet.clear();
this.filteredGraph.nodePredicateChanged();
}
const isTree = this.isTree();
const layout = isTree ? this.createConfiguredLayout(fromSketch) : this.createConfiguredNonTreeLayout(fromSketch);
const layoutData = isTree ? this.createConfiguredLayoutData(this.filteredGraph, new Set(incrementalNodes)) : this.createConfiguredNonTreeLayoutData(new Set(incrementalNodes));
this.filteredGraph.applyLayout(layout, layoutData);
this._graphComponent.fitGraphBounds();
}
isTree() {
return new GraphStructureAnalyzer(this.completeGraph).isTree();
}
/**
* Focuses the given item.
*
* If the item is currently not visible, it will be unhidden together with its descendants.
*/
zoomToItem(item) {
if (item instanceof IEdge) {
const source = item.sourceNode;
const target = item.targetNode;
this.unhideNode(source);
this.unhideNode(target);
this._graphComponent.currentItem = item;
this._graphComponent.zoomTo(Rect.add(source.layout.toRect(), target.layout.toRect()));
this._graphComponent.focus();
} else if (item instanceof INode) {
this.unhideNode(item);
this._graphComponent.currentItem = item;
this._graphComponent.executeCommand(Command.ZOOM_TO_CURRENT_ITEM, null);
this._graphComponent.focus();
}
}
/**
* Zooms to the union-bounds of the given items.
*
* If the item is currently not visible, it will be unhidden together with its descendants.
*/
zoomTo(items) {
const targetBounds = new MutableRectangle();
items.forEach((item) => {
if (item instanceof IEdge) {
const source = item.sourceNode;
const target = item.targetNode;
this.unhideNode(source);
this.unhideNode(target);
targetBounds.add(source.layout);
targetBounds.add(target.layout);
} else if (item instanceof INode) {
this.unhideNode(item);
targetBounds.add(item.layout);
}
});
this._graphComponent.focus();
this._graphComponent.zoomToAnimated(targetBounds.toRect().getEnlarged(200));
}
unhideNode(item) {
if (!this.filteredGraph.nodes.includes(item)) {
this.showItem(item);
}
}
showItem(item) {
this.hiddenNodesSet.clear();
this.addToHiddenNodes(_CollapsibleTree.collectAllNodesExceptSubtree(this.completeGraph, item));
this.filteredGraph.nodePredicateChanged();
const isTree = this.isTree();
this.filteredGraph.applyLayout(
isTree ? this.createConfiguredLayout(false) : this.createConfiguredNonTreeLayout(false),
isTree ? this.createConfiguredLayoutData(this.filteredGraph, /* @__PURE__ */ new Set()) : this.createConfiguredNonTreeLayoutData()
);
this.onGraphUpdated();
}
/**
* Refreshes the node after modifications on the tree.
* @returns a promise which is resolved when the layout has been executed.
*/
async refreshLayout(centerNode, incrementalNodes, collapse) {
if (this.doingLayout) {
return Promise.resolve();
}
this.doingLayout = true;
if (!collapse) {
this.prepareSmoothExpandLayoutAnimation(incrementalNodes);
}
const isTree = this.isTree();
const coreLayout = isTree ? this.createConfiguredLayout(true) : this.createConfiguredNonTreeLayout(true);
const layout = new LayoutAnchoringStage(coreLayout);
const layoutData = new CompositeLayoutData();
if (centerNode) {
layoutData.items.add(
new LayoutAnchoringStageData({
nodeAnchoringPolicies: (node) => centerNode === node ? LayoutAnchoringPolicy.CENTER : LayoutAnchoringPolicy.NONE
})
);
}
if (collapse) {
layoutData.items.add(
new PlaceNodesAtBarycenterStageData({
affectedNodes: incrementalNodes
})
);
}
layoutData.items.add(
isTree ? this.createConfiguredLayoutData(this.filteredGraph) : (
// for hierarchical layout, mark all descendants as incremental during expand,
// when collapsing no incremental nodes are needed
this.createConfiguredNonTreeLayoutData(collapse ? void 0 : incrementalNodes)
)
);
const executor = new LayoutExecutor({
graphComponent: this._graphComponent,
layout,
layoutData,
animateViewport: centerNode === null,
easedAnimation: true,
animationDuration: "0.5s",
portPlacementPolicies: PortPlacementPolicy.KEEP_PARAMETER,
targetBoundsPadding: 100
});
await executor.start();
this.doingLayout = false;
}
/**
* Moves incremental nodes to a location between their neighbors before expanding for a smooth animation.
*/
prepareSmoothExpandLayoutAnimation(incrementalNodes) {
const graph = this._graphComponent.graph;
const layoutData = new PlaceNodesAtBarycenterStageData({
affectedNodes: incrementalNodes
});
const layout = new PlaceNodesAtBarycenterStage();
graph.applyLayout(layout, layoutData);
}
/**
* Creates a {@link TreeLayoutData} for the tree layout
*/
createConfiguredLayoutData(graph = null, incrementalNodes = /* @__PURE__ */ new Set()) {
const hasIncrementalParent = (node) => graph.inDegree(node) > 0 && incrementalNodes.has(graph.predecessors(node).at(0));
const incrementalEdgesComparison = () => {
return (edge1, edge2) => {
const y1 = edge1.targetNode.layout.center.y;
const y2 = edge2.targetNode.layout.center.y;
if (y1 === y2) {
const x1 = edge1.targetNode.layout.center.x;
const x2 = edge2.targetNode.layout.center.x;
if (x1 === x2) {
return 0;
}
return x1 < x2 ? -1 : 1;
}
return y1 < y2 ? -1 : 1;
};
};
return new TreeLayoutData({
assistantNodes: (node) => this.isAssistantNode(node) && graph.inDegree(node) > 0 && !hasIncrementalParent(node),
childOrder: {
outEdgeComparators: incrementalNodes.size > 0 ? incrementalEdgesComparison : this.outEdgeComparison
},
nodeTypes: this.nodeTypesMapping,
compactSubtreePlacerStrategyMementos: this.compactSubtreePlacerStrategyMementos
});
}
createConfiguredNonTreeLayoutData(incrementalNodes = /* @__PURE__ */ new Set()) {
return new HierarchicalLayoutData({
sourceGroupIds: (edge) => edge.sourceNode + "_source",
incrementalNodes
});
}
createConfiguredNonTreeLayout(fromSketch) {
const hierarchicalLayout = new HierarchicalLayout({
fromSketchMode: fromSketch,
nodeToEdgeDistance: 20,
defaultEdgeDescriptor: new HierarchicalLayoutEdgeDescriptor({
minimumFirstSegmentLength: 20,
minimumLastSegmentLength: 20
})
});
hierarchicalLayout.layoutStages.append(new PlaceNodesAtBarycenterStage());
return hierarchicalLayout;
}
/**
* Creates a tree layout that handles assistant nodes and stack leaf nodes.
* @returns A configured TreeLayout.
*/
createConfiguredLayout(fromSketch) {
const treeLayout = new TreeLayout();
treeLayout.defaultPortAssigner = new class extends BaseClass(ITreeLayoutPortAssigner) {
assignPorts(graph, node) {
const inEdge = node.inEdges.first();
if (inEdge) {
inEdge.targetPortOffset = Point.ORIGIN;
}
const halfHeight = node.layout.size.height / 2;
for (const outEdge of node.outEdges) {
outEdge.sourcePortOffset = new Point(0, halfHeight);
}
}
}();
treeLayout.defaultSubtreePlacer = new CompactSubtreePlacer();
treeLayout.layoutStages.append(new PlaceNodesAtBarycenterStage());
return new TreeReductionStage(treeLayout);
}
addToHiddenNodes(nodes) {
for (const node of nodes) {
this.hiddenNodesSet.add(node);
}
}
/**
* Set the collapsed state to all the node's ports.
*/
updateCollapsedState(node, collapsed) {
for (const outEdge of this.completeGraph.outEdgesAt(node)) {
this.onCollapsedStateUpdated(outEdge.sourcePort, collapsed);
}
}
/**
* Restores the group containing the given node if needed.
*/
static restoreGroup(graph, hiddenNodesSet, node) {
const parent = graph.getParent(node);
if (parent && hiddenNodesSet.has(parent)) {
hiddenNodesSet.delete(parent);
}
}
/**
* Removes all groups in the given graph that will be empty after removing the given nodes.
*/
removeEmptyGroups(nodesToHide) {
const emptyGroups = _CollapsibleTree.findEmptyGroups(this.filteredGraph, nodesToHide).toArray();
for (const group of emptyGroups) {
this.hiddenNodesSet.add(group);
}
}
static findEmptyGroups(graph, nodesToHide) {
return graph.nodes.filter(
(node) => graph.isGroupNode(node) && graph.degree(node) === 0 && graph.getChildren(node).every((child) => nodesToHide.has(child))
);
}
/**
* @returns all descendants of the passed node excluding the node itself.
*/
static collectDescendants(graph, root) {
const nodes = /* @__PURE__ */ new Set();
const queue = [root];
while (queue.length > 0) {
const node = queue.pop();
for (const outEdge of graph.outEdgesAt(node)) {
queue.unshift(outEdge.targetNode);
nodes.add(outEdge.targetNode);
}
}
return nodes;
}
/**
* Creates an array of all nodes excluding the nodes in the subtree rooted in the excluded sub-root.
*/
static collectAllNodesExceptSubtree(graph, excludedRoot) {
const subtree = this.collectDescendants(graph, excludedRoot);
subtree.add(excludedRoot);
return new Set(graph.nodes.filter((node) => !subtree.has(node)));
}
/**
* Informs the listener that the graph was updated.
*/
onGraphUpdated() {
this.graphUpdatedListener?.();
}
/**
* Informs the listener that the collapsed state was updated.
*/
onCollapsedStateUpdated(port, collapsed) {
this.collapsedStateUpdatedListener?.(port, collapsed);
}
};
// src/OrgChartProvider.tsx
import { useGraphComponent, withGraphComponentProvider } from "@yworks/react-yfiles-core";
// src/OrgChartModel.ts
import { Command as Command2 } from "@yfiles/yfiles";
import {
exportImageAndSave,
exportSvgAndSave,
printDiagram
} from "@yworks/react-yfiles-core";
var defaultMargins = { top: 5, right: 5, left: 5, bottom: 5 };
function createOrgChartModel(collapsibleTree, graphComponent) {
let onRenderedCallback = null;
const setRenderedCallback = (cb) => {
onRenderedCallback = cb;
};
const onRendered = () => {
onRenderedCallback?.();
onRenderedCallback = null;
};
function zoomTo(items) {
if (items.length === 0) {
return;
}
const graph = graphComponent.graph;
const modelItems = [];
items.forEach((item) => {
if ("source" in item && "target" in item) {
const source = getNode(item.source, graph);
const target = getNode(item.target, graph);
const edge = graph.getEdge(source, target);
if (edge) {
modelItems.push(edge);
}
} else {
const node = getNode(item, graph);
modelItems.push(node);
}
});
collapsibleTree.zoomTo(modelItems);
}
return {
graphComponent,
async showAll() {
await collapsibleTree.executeShowAll();
},
canShowAll() {
return collapsibleTree.canExecuteShowAll();
},
async showSuperior(item) {
const node = getNode(item, graphComponent.graph);
if (node) {
await collapsibleTree.executeShowParent(node);
}
},
canShowSuperior(item) {
const node = getNode(item, graphComponent.graph);
if (node) {
return collapsibleTree.canExecuteShowParent(node);
}
return false;
},
async hideSuperior(item) {
const node = getNode(item, graphComponent.graph);
if (node) {
await collapsibleTree.executeHideParent(node);
}
},
canHideSuperior(item) {
const node = getNode(item, graphComponent.graph);
if (node) {
return collapsibleTree.canExecuteHideParent(node);
}
return false;
},
async showSubordinates(item) {
const node = getNode(item, graphComponent.graph);
if (node) {
await collapsibleTree.executeShowChildren(node);
}
},
canShowSubordinates(item) {
const node = getNode(item, graphComponent.graph);
if (node) {
return collapsibleTree.canExecuteShowChildren(node);
}
return false;
},
async hideSubordinates(item) {
const node = getNode(item, graphComponent.graph);
if (node) {
await collapsibleTree.executeHideChildren(node);
}
},
canHideSubordinates(item) {
const node = getNode(item, graphComponent.graph);
if (node) {
return collapsibleTree.canExecuteHideChildren(node);
}
return false;
},
applyLayout(incremental, incrementalItems) {
const incrementalNodes = [];
incrementalItems?.forEach((item) => {
const node = getNode(item, graphComponent.graph);
if (node) {
incrementalNodes.push(node);
}
});
return collapsibleTree.applyInitialLayout(incremental ?? false, incrementalNodes);
},
zoomToItem(item) {
zoomTo([item]);
},
zoomTo,
zoomIn() {
graphComponent.executeCommand(Command2.INCREASE_ZOOM, null);
},
zoomOut() {
graphComponent.executeCommand(Command2.DECREASE_ZOOM, null);
},
zoomToOriginal() {
graphComponent.executeCommand(Command2.ZOOM, 1);
},
fitContent() {
graphComponent.executeCommand(Command2.FIT_GRAPH_BOUNDS, null);
},
addGraphUpdatedListener(listener) {
collapsibleTree.addGraphUpdatedListener(listener);
},
removeGraphUpdatedListener(listener) {
collapsibleTree.removeGraphUpdatedListener(listener);
},
getVisibleItems() {
return collapsibleTree.graphComponent.graph.nodes.map((item) => item.tag).toArray();
},
async exportToSvg(exportSettings) {
const settings = exportSettings ?? {
zoom: graphComponent.zoom,
scale: graphComponent.zoom,
margins: defaultMargins,
inlineImages: true
};
await exportSvgAndSave(settings, graphComponent, setRenderedCallback);
},
async exportToPng(exportSettings) {
const settings = exportSettings ?? {
zoom: graphComponent.zoom,
scale: 1,
margins: defaultMargins
};
await exportImageAndSave(settings, graphComponent, setRenderedCallback);
},
async print(printSettings) {
const settings = printSettings ?? {
zoom: graphComponent.zoom,
scale: 1,
margins: defaultMargins
};
await printDiagram(settings, graphComponent);
},
refresh() {
graphComponent.invalidate();
},
getSearchHits: () => [],
// will be replaced during initialization
onRendered
};
}
function getNode(item, graph) {
return item ? graph.nodes.find((node) => node.tag.id === item.id) : null;
}
// src/OrgChartProvider.tsx
import { jsx } from "react/jsx-runtime";
var OrgChartContext = createContext(null);
function useOrgChartContextInternal() {
return useContext(OrgChartContext);
}
function useOrgChartContext() {
const context = useContext(OrgChartContext);
if (context === null) {
throw new Error(
"This method can only be used inside an OrgChart component or OrgChartProvider."
);
}
return context;
}
var gcToModel = /* @__PURE__ */ new WeakMap();
var OrgChartProvider = withGraphComponentProvider(({ children }) => {
const graphComponent = useGraphComponent();
if (!graphComponent) {
return children;
}
const orgChart = useMemo(() => {
if (gcToModel.has(graphComponent)) {
return gcToModel.get(graphComponent);
}
const collapsibleTree = new CollapsibleTree(graphComponent);
graphComponent.graph = collapsibleTree.filteredGraph;
collapsibleTree.isAssistantNode = (node) => node.tag?.assistant ?? false;
const orgChartModel = createOrgChartModel(collapsibleTree, graphComponent);
gcToModel.set(graphComponent, orgChartModel);
return orgChartModel;
}, [graphComponent]);
return /* @__PURE__ */ jsx(OrgChartContext.Provider, { value: orgChart, children });
});
// src/styles/Templates.tsx
import {
DefaultControlButtons
} from "@yworks/react-yfiles-core";
import { useMemo as useMemo2, useState } from "react";
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
function RenderOrgChartItem({
dataItem,
detail,
hovered,
focused,
selected
}) {
const customOrgChartItem = dataItem;
const properties = findProperties(customOrgChartItem);
return /* @__PURE__ */ jsx2(Fragment, { children: /* @__PURE__ */ jsx2(
"div",
{
style: { width: "100%", height: "100%", overflow: "hidden" },
className: getHighlightClasses(selected, hovered, focused),
children: detail === "high" ? /* @__PURE__ */ jsxs(
"div",
{
className: `${customOrgChartItem.className ?? ""} yfiles-react-detail-node`.trim(),
style: customOrgChartItem.style ?? {},
children: [
customOrgChartItem.status && /* @__PURE__ */ jsx2(
"div",
{
className: `yfiles-react-detail-node__status-bar yfiles-react-${customOrgChartItem.status}`
}
),
/* @__PURE__ */ jsxs("div", { className: "yfiles-react-detail-node__content", children: [
customOrgChartItem.icon && /* @__PURE__ */ jsx2("div", { className: "yfiles-react-detail-node__icon-container", children: /* @__PURE__ */ jsx2(
"img",
{
src: customOrgChartItem.icon,
alt: "icon",
className: "yfiles-react-detail-node__icon"
}
) }),
/* @__PURE__ */ jsxs("div", { className: "yfiles-react-detail-node__data-container", children: [
customOrgChartItem.name && /* @__PURE__ */ jsx2("div", { className: "yfiles-react-detail-node__name", children: customOrgChartItem.name }),
customOrgChartItem.position && /* @__PURE__ */ jsx2("div", { className: "yfiles-react-detail-node__position", children: customOrgChartItem.position }),
customOrgChartItem.email && /* @__PURE__ */ jsx2("div", { children: customOrgChartItem.email }),
customOrgChartItem.phone && /* @__PURE__ */ jsx2("div", { children: customOrgChartItem.phone }),
properties.filter(
(property) => [
"id",
"className",
"style",
"subordinates",
"width",
"height",
"assistant"
].every((key) => property !== key)
).map((property, i) => (
// @ts-ignore
/* @__PURE__ */ jsx2("div", { children: stringifyData(customOrgChartItem[property]) }, i)
))
] })
] })
]
}
) : /* @__PURE__ */ jsx2(
"div",
{
className: `yfiles-react-overview-node yfiles-react-${customOrgChartItem.status ?? ""} ${customOrgChartItem.className ?? ""}`.trim(),
style: customOrgChartItem.style ?? {},
children: customOrgChartItem.name ?? customOrgChartItem.id
}
)
}
) });
}
function getHighlightClasses(selected, hovered, focused) {
const highlights = ["yfiles-react-node-highlight"];
if (focused) {
highlights.push("yfiles-react-node-highlight--focused");
}
if (hovered) {
highlights.push("yfiles-react-node-highlight--hovered");
}
if (selected) {
highlights.push("yfiles-react-node-highlight--selected");
}
return highlights.join(" ");
}
function findProperties(data) {
const defaultProperties = ["position", "name", "email", "phone", "icon", "status"];
return Object.keys(data).sort((property1, property2) => {
const p1 = defaultProperties.indexOf(property1);
const p2 = defaultProperties.indexOf(property2);
if (p1 >= 0 && p2 >= 0) {
return p1 - p2;
} else if (p1 >= 0) {
return -1;
} else if (p2 >= 0) {
return 1;
} else {
return 0;
}
}).slice(0, 6).filter((property) => !defaultProperties.includes(property));
}
function RenderOrgChartTooltip({
data
}) {
if ("source" in data && "target" in data) {
return null;
}
return /* @__PURE__ */ jsx2("div", { className: "yfiles-react-tooltip", children: stringifyData("name" in data ? data.name : data.id) });
}
function RenderOrgChartPopup({
item,
onClose
}) {
return /* @__PURE__ */ jsxs("div", { className: "yfiles-react-popup__content", children: [
stringifyData("name" in item ? item.name : item.id),
/* @__PURE__ */ jsx2("button", { onClick: () => onClose(), children: "x" })
] });
}
function OrgChartControlButtons() {
const items = DefaultControlButtons();
const orgChart = useOrgChartContext();
const [showAllDisabled, setShowAllDisabled] = useState(!orgChart.canShowAll());
useMemo2(() => {
orgChart.addGraphUpdatedListener(() => {
setShowAllDisabled(!orgChart.canShowAll());
});
}, []);
items.push({
className: "yfiles-react-controls__button--show-all",
action: () => orgChart.showAll(),
tooltip: "Show All",
disabled: showAllDisabled
});
return items;
}
function OrgChartContextMenuItems(item) {
const orgChart = useOrgChartContext();
const items = [];
if (item) {
const superior = "source" in item ? item.source : item;
const subordinate = "target" in item ? item.target : item;
if (orgChart.canHideSuperior(subordinate)) {
items.push({
title: "Hide Superior",
action: () => {
void orgChart?.hideSuperior(subordinate);
}
});
}
if (orgChart.canShowSuperior(subordinate)) {
items.push({
title: "Show Superior",
action: () => {
void orgChart?.showSuperior(subordinate);
}
});
}
if (orgChart.canHideSubordinates(superior)) {
items.push({
title: "Hide Subordinates",
action: () => {
void orgChart?.hideSubordinates(superior);
}
});
}
if (orgChart.canShowSubordinates(superior)) {
items.push({
title: "Show Subordinates",
action: () => {
void orgChart?.showSubordinates(superior);
}
});
}
}
if (orgChart.canShowAll()) {
items.push({
title: "Show all",
action: () => {
void orgChart?.showAll();
}
});
}
return items;
}
function stringifyData(data) {
return typeof data === "object" ? JSON.stringify(data) : String(data);
}
// src/OrgChart.tsx
import {
useEffect,
useLayoutEffect,
useMemo as useMemo3,
useState as useState2
} from "react";
import {
Arrow,
ArrowType,
PolylineEdgeStyle,
Size as Size2
} from "@yfiles/yfiles";
import {
checkLicense,
checkStylesheetLoaded,
ContextMenu,
LicenseError,
Popup,
ReactComponentHtmlNodeStyle as ReactComponentHtmlNodeStyle2,
ReactNodeRendering,
Tooltip,
useGraphSearch,
useReactNodeRendering,
withGraphComponent
} from "@yworks/react-yfiles-core";
// src/core/input.ts
import {
GraphItemTypes as GraphItemTypes2,
GraphViewerInputMode,
INode as INode3,
IPort,
ModifierKeys,
IPortStyle
} from "@yfiles/yfiles";
// src/core/data-loading.ts
import {
AdjacencyGraphBuilder,
Cycle,
EdgeCreator
} from "@yfiles/yfiles";
import {
convertToPolylineEdgeStyle,
ReactComponentHtmlNodeStyle
} from "@yworks/react-yfiles-core";
var GraphManager = class {
constructor(graphBuilder, nodesSource) {
this.graphBuilder = graphBuilder;
this.nodesSource = nodesSource;
}
data = [];
renderItem;
connectionStyles;
incrementalElements = [];
updateGraph(data, renderItem, connectionStyles) {
this.incrementalElements = compareData(this.data, data);
this.data = data;
if (this.nodesSource && this.graphBuilder) {
if (renderItem) {
this.renderItem = renderItem;
}
if (connectionStyles) {
this.connectionStyles = connectionStyles;
}
this.graphBuilder.setData(this.nodesSource, data);
this.graphBuilder.updateGraph();
const cycle = new Cycle().run(this.graphBuilder.graph);
if (cycle.edges.size > 0) {
throw new Error("Organization Chart data must not contain cycles");
}
}
}
};
function initializeGraphManager(graph, setNodeInfos) {
graph.clear();
const graphManager = new GraphManager();
const graphBuilder = new AdjacencyGraphBuilder(graph);
const nodesSource = graphBuilder.createNodesSource([], "id");
const edgeCreator = new EdgeCreator({
defaults: graphBuilder.graph.edgeDefaults
});
nodesSource.addOutEdgesSourceToId(
(item) => item.subordinates?.map((target) => {
return {
source: item,
target: graphManager.data.find((item2) => item2.id === target)
};
}) ?? [],
(item) => item.target?.id,
edgeCreator
);
edgeCreator.styleProvider = (edge) => {
if (graphManager.connectionStyles) {
const edgeStyle = graphManager.connectionStyles(edge.source, edge.target);
if (edgeStyle) {
return convertToPolylineEdgeStyle(edgeStyle);
}
}
return null;
};
nodesSource.nodeCreator.styleProvider = () => {
if (graphManager.renderItem) {
return new ReactComponentHtmlNodeStyle(graphManager.renderItem, setNodeInfos);
}
return null;
};
nodesSource.nodeCreator.layoutBindings.addBinding(
"width",
(item) => item.width ?? graph.nodeDefaults.size.width
);
nodesSource.nodeCreator.layoutBindings.addBinding(
"height",
(item) => item.height ?? graph.nodeDefaults.size.height
);
nodesSource.nodeCreator.layoutBindings.addBinding(
"x",
(item) => getNode(item, graph)?.layout.x ?? 0
);
nodesSource.nodeCreator.layoutBindings.addBinding(
"y",
(item) => getNode(item, graph)?.layout.y ?? 0
);
nodesSource.nodeCreator.addEventListener("node-updated", (evt) => {
nodesSource.nodeCreator.updateLayout(evt.graph, evt.item, evt.dataItem);
nodesSource.nodeCreator.updateStyle(evt.graph, evt.item, evt.dataItem);
nodesSource.nodeCreator.updateTag(evt.graph, evt.item, evt.dataItem);
nodesSource.nodeCreator.updateLabels(evt.graph, evt.item, evt.dataItem);
});
edgeCreator.addEventListener("edge-updated", (evt) => {
edgeCreator.updateStyle(evt.graph, evt.item, evt.dataItem);
edgeCreator.updateTag(evt.graph, evt.item, evt.dataItem);
edgeCreator.updateLabels(evt.graph, evt.item, evt.dataItem);
});
graphManager.graphBuilder = graphBuilder;
graphManager.nodesSource = nodesSource;
return graphManager;
}
function getOrgChartItem(node) {
return node.tag;
}
function compareData(oldData, newData) {
const unequalElements = [];
newData.forEach((obj2) => {
const matchingObject = oldData.find((obj1) => JSON.stringify(obj1) === JSON.stringify(obj2));
if (!matchingObject) {
unequalElements.push(obj2);
}
});
return unequalElements;
}
// src/core/SingleSelectionHelper.ts
import {
EventRecognizers,
GraphItemTypes,
Command as Command3,
IModelItem
} from "@yfiles/yfiles";
var commandBindings = [];
var oldMultiSelectionRecognizer = null;
function enableSingleSelection(graphComponent) {
const mode = graphComponent.inputMode;
oldMultiSelectionRecognizer = mode.multiSelectionRecognizer;
mode.marqueeSelectionInputMode.enabled = false;
mode.multiSelectionRecognizer = EventRecognizers.NEVER;
mode.availableCommands.remove(Command3.TOGGLE_ITEM_SELECTION);
mode.availableCommands.remove(Command3.SELECT_ALL);
mode.navigationInputMode.availableCommands.remove(Command3.EXTEND_SELECTION_LEFT);
mode.navigationInputMode.availableCommands.remove(Command3.EXTEND_SELECTION_UP);
mode.navigationInputMode.availableCommands.remove(Command3.EXTEND_SELECTION_DOWN);
mode.navigationInputMode.availableCommands.remove(Command3.EXTEND_SELECTION_RIGHT);
commandBindings.push(
mode.keyboardInputMode.addCommandBinding(Command3.EXTEND_SELECTION_LEFT, () => {
})
);
commandBindings.push(
mode.keyboardInputMode.addCommandBinding(Command3.EXTEND_SELECTION_UP, () => {
})
);
commandBindings.push(
mode.keyboardInputMode.addCommandBinding(Command3.EXTEND_SELECTION_DOWN, () => {
})
);
commandBindings.push(
mode.keyboardInputMode.addCommandBinding(Command3.EXTEND_SELECTION_RIGHT, () => {
})
);
commandBindings.push(
mode.keyboardInputMode.addCommandBinding(
Command3.TOGGLE_ITEM_SELECTION,
(event) => toggleItemSelectionExecuted(graphComponent, event),
(event) => toggleItemSelectionCanExecute(graphComponent, event)
)
);
graphComponent.selection.clear();
}
function toggleItemSelectionCanExecute(graphComponent, parameter) {
const modelItem = parameter instanceof IModelItem ? parameter : graphComponent.currentItem;
return modelItem !== null;
}
function toggleItemSelectionExecuted(graphComponent, parameter) {
const modelItem = parameter instanceof IModelItem ? parameter : graphComponent.currentItem;
const inputMode = graphComponent.inputMode;
if (modelItem === null || !graphComponent.graph.contains(modelItem) || !GraphItemTypes.itemIsOfTypes(inputMode.selectableItems, modelItem) || !inputMode.graphSelection) {
return false;
}
const isSelected = inputMode.graphSelection.includes(modelItem);
if (isSelected) {
inputMode.graphSelection.clear();
} else {
inputMode.graphSelection.clear();
inputMode.setSelected(modelItem, true);
}
return true;
}
// src/core/input.ts
function initializeInputMode(graphComponent, orgChart) {
const graphViewerInputMode = new GraphViewerInputMode({
clickableItems: GraphItemTypes2.NODE | GraphItemTypes2.PORT,
selectableItems: GraphItemTypes2.NODE,
marqueeSelectableItems: GraphItemTypes2.NONE,
toolTipItems: GraphItemTypes2.NONE,
contextMenuItems: GraphItemTypes2.NODE | GraphItemTypes2.EDGE,
focusableItems: GraphItemTypes2.NODE,
clickHitTestOrder: [GraphItemTypes2.PORT, GraphItemTypes2.NODE]
});
graphViewerInputMode.addEventListener("item-double-clicked", (evt) => {
const item = evt.item;
if (item instanceof INode3) {
orgChart.zoomToItem(getOrgChartItem(item));
}
});
initializeHighlights(graphComponent);
graphComponent.inputMode = graphViewerInputMode;
enableSingleSelection(graphComponent);
}
function initializeHover(onHover, graphComponent) {
const inputMode = graphComponent.inputMode;
inputMode.itemHoverInputMode.hoverItems = GraphItemTypes2.NODE;
const hoverItemChangedListener = (evt) => {
const manager = graphComponent.highlightIndicatorManager;
if (evt.oldItem) {
manager.items?.remove(evt.oldItem);
}
if (evt.item) {
manager.items?.add(evt.item);
}
if (onHover) {
onHover(evt.item?.tag, evt.oldItem?.tag);
}
};
inputMode.itemHoverInputMode.addEventListener("hovered-item-changed", hoverItemChangedListener);
return hoverItemChangedListener;
}
function initializeInteractivity(graphComponent, orgChartGraph, completeGraph) {
const graphViewerInputMode = graphComponent.inputMode;
initializeClickablePorts(graphViewerInputMode, orgChartGraph, completeGraph);
initializeKeyboardInputMode(graphViewerInputMode.keyboardInputMode, graphComponent, orgChartGraph);
}
function initializeFocus(onFocus, graphComponent) {
let currentItemChangedListener = () => {
};
if (onFocus) {
currentItemChangedListener = () => {
const currentItem = graphComponent.currentItem;
if (currentItem instanceof INode3) {
onFocus(getOrgChartItem(currentItem));
} else {
onFocus(null);
}
};
}
graphComponent.addEventListener("current-item-changed", currentItemChangedListener);
return currentItemChangedListener;
}
function initializeSelection(onSelect, graphComponent) {
let itemSelectionChangedListener = () => {
};
if (onSelect) {
itemSelectionChangedListener = () => {
const selectedItems = graphComponent.selection.nodes.map((node) => getOrgChartItem(node)).toArray();
onSelect(selectedItems);
};
}
graphComponent.selection.addEventListener("item-added", itemSelectionChangedListener);
graphComponent.selection.addEventListener("item-removed", itemSelectionChangedListener);
return itemSelectionChangedListener;
}
function initializeHighlights(graphComponent) {
graphComponent.graph.decorator.nodes.selectionRenderer.hide();
graphComponent.graph.decorator.nodes.focusRenderer.hide();
}
function initializeClickablePorts(graphViewerInputMode, orgChartGraph, completeGraph) {
graphViewerInputMode.clickableItems = GraphItemTypes2.NODE | GraphItemTypes2.PORT;
graphViewerInputMode.clickHitTestOrder = [GraphItemTypes2.PORT, GraphItemTypes2.NODE];
graphViewerInputMode.addEventListener("item-clicked", (evt) => {
const port = evt.item;
if (port instanceof IPort && completeGraph.inEdgesAt(port).size === 0 && port.style !== IPortStyle.VOID_PORT_STYLE) {
const node = port.owner;
if (node instanceof INode3) {
const item = getOrgChartItem(node);
if (orgChartGraph.canShowSubordinates(item)) {
void orgChartGraph.showSubordinates(item);
} else {
if (orgChartGraph.canHideSubordinates(item)) {
void orgChartGraph.hideSubordinates(item);
}
}
}
evt.handled = true;
}
});
}
function initializeKeyboardInputMode(keyboardInputMode, graphComponent, orgChartGraph) {
keyboardInputMode.addKeyBinding("*", ModifierKeys.NONE, () => {
if (orgChartGraph.canShowAll()) {
void orgChartGraph.showAll();
}
});
keyboardInputMode.addKeyBinding("-", ModifierKeys.NONE, () => {
if (graphComponent.currentItem instanceof INode3 && orgChartGraph.canHideSubordinates(getOrgChartItem(graphComponent.currentItem))) {
void orgChartGraph.hideSubordinates(getOrgChartItem(graphComponent.currentItem));
return true;
}
return false;
});
keyboardInputMode.addKeyBinding("+", ModifierKeys.NONE, () => {
if (graphComponent.currentItem instanceof INode3 && orgChartGraph.canShowSubordinates(getOrgChartItem(graphComponent.currentItem))) {
void orgChartGraph.showSubordinates(getOrgChartItem(graphComponent.currentItem));
return true;
}
return false;
});
keyboardInputMode.addKeyBinding("PageDown", ModifierKeys.NONE, () => {
if (graphComponent.currentItem instanceof INode3 && orgChartGraph.canHideSuperior(getOrgChartItem(graphComponent.currentItem))) {
void orgChartGraph.hideSuperior(getOrgChartItem(graphComponent.currentItem));
return true;
}
return false;
});
keyboardInputMode.addKeyBinding("PageUp", ModifierKeys.NONE, () => {
if (graphComponent.currentItem instanceof INode3 && orgChartGraph.canShowSuperior(getOrgChartItem(graphComponent.currentItem))) {
void orgChartGraph.showSuperior(getOrgChartItem(graphComponent.currentItem));
return true;
}
return false;
});
}
// src/styles/orgchart-port-style.ts
import { FreeNodePortLocationModel, IPortStyle as IPortStyle2, Size } from "@yfiles/yfiles";
// src/styles/CollapseExpandPortStyle.ts
import {
PortStyleBase,
Rect as Rect2,
SvgVisual
} from "@yfiles/yfiles";
var CollapseExpandPortStyle = class extends PortStyleBase {
constructor(renderSize, isCollapsed) {
super();
this.renderSize = renderSize;
this.isCollapsed = isCollapsed;
}
createVisual(_context, port) {
const halfWidth = this.renderSize.width * 0.5;
const halfHeight = this.renderSize.height * 0.5;
const collapsed = this.isCollapsed(port);
const container = document.createElementNS("http://www.w3.org/2000/svg", "g");
const portElement = document.createElementNS("http://www.w3.org/2000/svg", "g");
portElement.classList.add("yfiles-react-port");
container.appendChild(portElement);
const ellipse = document.createElementNS("http://www.w3.org/2000/svg", "ellipse");
ellipse.setAttribute("rx", String(halfWidth - 2));
ellipse.setAttribute("ry", String(halfHeight - 2));
portElement.appendChild(ellipse);
const horizontalLine = document.createElementNS("http://www.w3.org/2000/svg", "line");
horizontalLine.classList.add("yfiles-react-port__icon");
horizontalLine.setAttribute("x1", String(-(halfWidth - 4)));
horizontalLine.setAttribute("y1", "0");
horizontalLine.setAttribute("x2", String(+(halfWidth - 4)));
horizontalLine.setAttribute("y2", "0");
portElement.appendChild(horizontalLine);
const verticalLine = document.createElementNS("http://www.w3.org/2000/svg", "line");
verticalLine.setAttribute(
"class",
`yfiles-react-port__icon ${collapsed ? "yfiles-react-port__icon--expand" : "yfiles-react-port__icon--collapse"}`
);
verticalLine.setAttribute("x1", "0");
verticalLine.setAttribute("y1", "-1");
verticalLine.setAttribute("x2", "0");
verticalLine.setAttribute("y2", "1");
portElement.appendChild(verticalLine);
SvgVisual.setTranslate(container, port.location.x, port.location.y);
return SvgVisual.from(container, {
collapsed
});
}
updateVisual(_context, oldVisual, port) {
const container = oldVisual.svgElement;
const collapsed = this.isCollapsed(port);
if (oldVisual.tag.collapsed !== collapsed) {
container.lastElementChild.lastElementChild.setAttribute(
"class",
`yfiles-react-port__icon ${collapsed ? "yfiles-react-port__icon--expand" : "yfiles-react-port__icon--collapse"}`
);
oldVisual.tag.collapsed = collapsed;
}
SvgVisual.setTranslate(container, port.location.x, port.location.y);
return oldVisual;
}
getBounds(_context, port) {
const { width, height } = this.renderSize;
return new Rect2(port.location.x - width * 0.5, port.location.y - height * 0.5, width, height);
}
};
// src/styles/orgchart-port-style.ts
function setPortStylesToFirstOutgoingPorts(graph, portsVisible) {
const filteredGraph = graph;
const completeGraph = filteredGraph.wrappedGraph;
for (const node of completeGraph.nodes) {
const outEdges = completeGraph.outEdgesAt(node);
if (outEdges.size > 0) {
const firstOutgoingPort = outEdges.first().sourcePort;
const portStyle = portsVisible ? new CollapseExpandPortStyle(
new Size(20, 20),
(port) => completeGraph.edgesAt(port).size !== filteredGraph.edgesAt(port).size
) : IPortStyle2.VOID_PORT_STYLE;
completeGraph.setStyle(firstOutgoingPort, portStyle);
completeGraph.setPortLocationParameter(firstOutgoingPort, FreeNodePortLocationModel.BOTTOM);
}
}
}
// src/OrgChart.tsx
import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
var licenseErrorCodeSample = `import {OrgChart, registerLicense} from '@yworks/react-yfiles-orgchart'
import '@yworks/react-yfiles-orgchart/dist/index.css'
import yFilesLicense from './license.json'
function App() {
registerLicense(yFilesLicense)
const data = [
{id: 0, name: 'Eric Joplin', subordinates: [1, 2]},
{id: 1, name: 'Amy Kain'},
{id: 2, name: 'David Kerry'}
]
return <OrgChart data={data}></OrgChart>
}`;
function OrgChart(props) {
if (!checkLicense()) {
return /* @__PURE__ */ jsx3(
LicenseError,
{
componentName: "yFiles React Organization Chart Component",
codeSample: licenseErrorCodeSample
}
);
}
const isWrapped = useOrgChartContextInternal();
if (isWrapped) {
return /* @__PURE__ */ jsx3(OrgChartCore, { ...props, children: props.children });
}
return /* @__PURE__ */ jsx3(OrgChartProvider, { children: /* @__PURE__ */ jsx3(OrgChartCore, { ...props, children: props.children }) });
}
var OrgChartCore = withGraphComponent(
({
children,
interactive = true,
renderItem,
connectionStyles,
onItemHover,
onSearch,
onItemFocus,
onItemSelect,
data,
searchNeedle,
itemSize,
renderTooltip,
contextMenuItems,
renderContextMenu,
popupPosition,
renderPopup,
incrementalLayout
}) => {
const orgChartGraph = useOrgChartContext();
const graphComponent = orgChartGraph.graphComponent;
const { nodeInfos, setNodeInfos } = useReactNodeRendering();
const { graphManager } = useMemo3(() => {
const filteredGraph = graphComponent.graph;
const completeGraph = filteredGraph.wrappedGraph;
initializeDefaultStyle(graphComponent, completeGraph, setNodeInfos, itemSize);
const graphManager2 = initializeGraphManager(completeGraph, setNodeInfos);
initializeInputMode(graphComponent, orgChartGraph);
return {
graphManager: graphManager2
};
}, []);
useEffect(() => {
checkStylesh