UNPKG

@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
// 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 hierarchic 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 hierarchicLayout = new HierarchicalLayout({ fromSketchMode: fromSketch, nodeToEdgeDistance: 20, defaultEdgeDescriptor: new HierarchicalLayoutEdgeDescriptor({ minimumFirstSegmentLength: 20, minimumLastSegmentLength: 20 }) }); hierarchicLayout.layoutStages.append(new PlaceNodesAtBarycenterStage()); return hierarchicLayout; } /** * 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(() => { checkStylesheetLoade