@yworks/yfiles-layout-reactflow
Version:
yFiles Layouts for React Flow - A layout library for React Flow providing powerful yFiles layout algorithms and supporting components
307 lines (303 loc) • 10.4 kB
JavaScript
// src/layout/WebWorkerSupport.ts
import {
LayoutExecutorAsyncWorker,
License,
MinimumNodeSizeStage
} from "@yfiles/yfiles";
// src/layout/layout-algorithms.ts
import {
CircularLayout,
CircularLayoutData,
EdgeLabelPreferredPlacement as YFilesEdgeLabelPreferredPlacement,
EdgePortCandidates,
EdgeRouter,
EdgeRouterData,
GenericLabeling,
GenericLabelingData,
GenericLayoutData,
HierarchicalLayout,
HierarchicalLayoutData,
Insets as YFilesInsets,
LayoutKeys,
OrganicEdgeRouter,
OrganicLayout,
OrganicLayoutData,
OrthogonalLayout,
OrthogonalLayoutData,
RadialLayout,
RadialLayoutData,
RadialTreeLayout,
RadialTreeLayoutData,
TreeLayout,
TreeLayoutData,
TreeReductionStage
} from "@yfiles/yfiles";
async function getLayoutAlgorithm(layoutDescriptor) {
switch (layoutDescriptor.name) {
case "GenericLabeling": {
return new GenericLabeling(layoutDescriptor.properties ?? {});
}
case "RadialTreeLayout": {
return new TreeReductionStage({
coreLayout: new RadialTreeLayout(layoutDescriptor.properties ?? {}),
nonTreeEdgeRouter: new OrganicEdgeRouter()
});
}
case "HierarchicalLayout": {
return new HierarchicalLayout(layoutDescriptor.properties ?? {});
}
case "CircularLayout": {
return new CircularLayout(layoutDescriptor.properties ?? {});
}
case "OrganicLayout": {
return new OrganicLayout(layoutDescriptor.properties ?? {});
}
case "OrthogonalLayout": {
return new OrthogonalLayout(layoutDescriptor.properties ?? {});
}
case "EdgeRouter": {
return new EdgeRouter(layoutDescriptor.properties ?? {});
}
case "RadialLayout": {
return new RadialLayout(layoutDescriptor.properties ?? {});
}
case "TreeLayout": {
return new TreeReductionStage({
coreLayout: new TreeLayout(layoutDescriptor.properties ?? {}),
nonTreeEdgeRouter: new EdgeRouter()
});
}
default: {
return new OrganicLayout();
}
}
}
function getLayoutData(layoutName, layoutDataDescriptor, reactFlowRef) {
const layoutData = new GenericLayoutData();
const rootElement = getRootNode(reactFlowRef);
layoutData.addItemMapping(
LayoutKeys.GROUP_NODE_PADDING_DATA_KEY,
(node) => (
// @ts-expect-error property-groupNodePadding-does-not-exist-on-type-LayoutDataProvider
translatePadding(rootElement, node, layoutDataDescriptor?.groupNodePadding?.(node.tag))
)
);
switch (layoutName) {
case "GenericLabeling":
layoutData.add(new GenericLabelingData(translateLayoutDataDescriptor(layoutDataDescriptor)));
break;
case "RadialTreeLayout":
layoutData.add(new RadialTreeLayoutData(translateLayoutDataDescriptor(layoutDataDescriptor)));
break;
case "HierarchicalLayout":
layoutData.add(
new HierarchicalLayoutData(translateLayoutDataDescriptor(layoutDataDescriptor))
);
break;
case "CircularLayout":
layoutData.add(new CircularLayoutData(translateLayoutDataDescriptor(layoutDataDescriptor)));
break;
case "OrganicLayout":
layoutData.add(new OrganicLayoutData(translateLayoutDataDescriptor(layoutDataDescriptor)));
break;
case "OrthogonalLayout":
layoutData.add(new OrthogonalLayoutData(translateLayoutDataDescriptor(layoutDataDescriptor)));
break;
case "EdgeRouter":
layoutData.add(new EdgeRouterData(translateLayoutDataDescriptor(layoutDataDescriptor)));
break;
case "RadialLayout":
layoutData.add(new RadialLayoutData(translateLayoutDataDescriptor(layoutDataDescriptor)));
break;
case "TreeLayout":
layoutData.add(new TreeLayoutData(translateLayoutDataDescriptor(layoutDataDescriptor)));
break;
default:
}
return layoutData;
}
function translateLayoutDataDescriptor(layoutDataDescriptor) {
if (!layoutDataDescriptor) {
return {};
}
const translatedLayoutDataDescriptor = {};
Object.keys(layoutDataDescriptor).forEach((key) => {
const originalFunction = layoutDataDescriptor[key];
if (originalFunction) {
if (layoutDataDescriptor.groupNodePadding) {
} else if (key === "edgeLabelPreferredPlacements") {
translatedLayoutDataDescriptor[key] = (item) => translateEdgeLabelPreferredPlacement(
originalFunction(item.tag)
);
} else if (key === "nodeMargins") {
translatedLayoutDataDescriptor[key] = (item) => translateNodeMargins(originalFunction(item.tag));
} else if (key === "ports") {
translatedLayoutDataDescriptor[key] = translatePortDataProvider(
// @ts-expect-error property-ports-does-not-exist-on-type-LayoutDataProvider
layoutDataDescriptor[key]
);
} else if (key === "childOrder") {
translatedLayoutDataDescriptor[key] = translateChildOrderDataProvider(
// @ts-expect-error property-childOrder-does-not-exist-on-type-LayoutDataProvider
layoutDataDescriptor[key]
);
} else if (key === "scope") {
translatedLayoutDataDescriptor[key] = translateScopeDataProvider(
// @ts-expect-error property-scope-does-not-exist-on-type-LayoutDataProvider
layoutDataDescriptor[key]
);
} else {
translatedLayoutDataDescriptor[key] = (item) => originalFunction(item.tag);
}
}
});
return translatedLayoutDataDescriptor;
}
function translateChildOrderDataProvider(childOrder) {
const translatedChildOrderData = {};
Object.keys(childOrder).forEach((key) => {
const originalFunction = childOrder[key];
translatedChildOrderData[key] = (node) => translateOutEdgeComparers(
originalFunction(node.tag)
);
});
return translatedChildOrderData;
}
function translateOutEdgeComparers(outEdgeComparer) {
return (e1, e2) => outEdgeComparer(e1.tag, e2.tag);
}
function translatePortDataProvider(ports) {
const translatedPortData = {};
Object.keys(ports).forEach((key) => {
const originalFunction = ports[key];
translatedPortData[key] = key === "sourcePortCandidates" || key === "targetPortCandidates" ? (item) => translatePortCandidates(originalFunction(item.tag)) : (item) => originalFunction(item.tag);
});
return translatedPortData;
}
function translatePortCandidates(sides) {
const portCandidates = new EdgePortCandidates();
sides.forEach((side) => {
portCandidates.addFreeCandidate(side);
});
return portCandidates;
}
function translateScopeDataProvider(scope) {
const translatedScopeData = {};
Object.keys(scope).forEach((key) => {
const originalFunction = scope[key];
translatedScopeData[key] = (item) => originalFunction(item.tag);
});
return translatedScopeData;
}
function translateNodeMargins(insets) {
if (typeof insets === "number") {
return new YFilesInsets(insets);
}
return new YFilesInsets(insets.top, insets.right, insets.bottom, insets.left);
}
function translatePadding(rootElement, node, insets) {
if (insets) {
if (typeof insets === "number") {
return new YFilesInsets(insets, insets, insets, insets);
}
return new YFilesInsets(insets.top, insets.right, insets.bottom, insets.left);
}
return getGroupNodePadding(node, rootElement);
}
function translateEdgeLabelPreferredPlacement(descriptor) {
return new YFilesEdgeLabelPreferredPlacement(descriptor);
}
function getGroupNodePadding(node, rootElement) {
const nodeElement = getNodeElementById(rootElement, node.tag.id);
if (nodeElement) {
const computedStyle = window.getComputedStyle(nodeElement);
const paddingTop = parseInt(computedStyle.getPropertyValue("padding-top")) ?? 0;
const paddingLeft = parseInt(computedStyle.getPropertyValue("padding-left")) ?? 0;
const paddingBottom = parseInt(computedStyle.getPropertyValue("padding-bottom")) ?? 0;
const paddingRight = parseInt(computedStyle.getPropertyValue("padding-right")) ?? 0;
return new YFilesInsets(paddingTop, paddingLeft, paddingBottom, paddingRight);
}
return new YFilesInsets(0, 0, 0, 0);
}
function getNodeElementById(rootElement, id) {
const elements = rootElement.querySelectorAll(`[data-id="${id}"]`);
if (elements.length > 1) {
const element = elements.values().find((element2) => element2.classList.toString().includes("react-flow__node"));
if (element) {
return element;
}
}
return elements.item(0);
}
function getRootNode(reactFlowRef) {
if (reactFlowRef?.current) {
return reactFlowRef.current.getRootNode();
}
const shadowRoots = Array.from(document.querySelectorAll("*")).filter((e) => !!e.shadowRoot).map((e) => e.shadowRoot);
if (shadowRoots.length > 0) {
if (shadowRoots.length > 1) {
console.warn(
"There are more than one shadow roots. For correct results, please call useLayout or useLayoutSupport with a reference to the corresponding reactflow component."
);
}
return shadowRoots.at(0);
}
return document;
}
// src/layout/WebWorkerSupport.ts
var license = null;
function setWebWorkerLicense(licensePar) {
license = licensePar;
}
function registerWebWorker(worker) {
if (license === null) {
throw new Error("License not initialized.");
}
return new Promise((resolve) => {
worker.onmessage = (event) => {
if (event.data === "ready") {
worker.postMessage({
license
});
} else if (event.data === "licensed") {
resolve(worker);
}
};
worker.postMessage("check-is-ready");
});
}
function initializeWebWorker(self) {
self.addEventListener(
"message",
(e) => {
if (e.data.license) {
License.value = e.data.license;
self.postMessage("licensed");
return;
}
if (e.data === "check-is-ready") {
self.postMessage("ready");
return;
}
new LayoutExecutorAsyncWorker(applyLayout).process(e.data).then((data) => {
self.postMessage(data);
}).catch((errorObj) => {
self.postMessage(errorObj);
});
},
false
);
self.postMessage("ready");
}
async function applyLayout(graph, layoutDescriptor) {
const layout = await getLayoutAlgorithm(layoutDescriptor);
new MinimumNodeSizeStage(layout).applyLayout(graph);
}
export {
getLayoutAlgorithm,
getLayoutData,
getRootNode,
setWebWorkerLicense,
registerWebWorker,
initializeWebWorker
};