@yworks/react-yfiles-core
Version:
This module provides shared functionality for the yFiles React components.
1,459 lines (1,426 loc) • 117 kB
JavaScript
// src/context-menu/ContextMenu.tsx
import {
createElement,
useCallback,
useLayoutEffect as useLayoutEffect2,
useState
} from "react";
// src/graph-component/GraphComponentProvider.tsx
import {
createContext,
useContext,
useLayoutEffect,
useMemo
} from "react";
import { GraphComponent, ScrollBarVisibility } from "@yfiles/yfiles";
import { jsx } from "react/jsx-runtime";
var GraphComponentContext = createContext(null);
function useGraphComponent() {
const graphComponent = useContext(GraphComponentContext);
if (!graphComponent) {
throw new Error("GraphComponent is not available in this context.");
}
return graphComponent;
}
function withGraphComponentProvider(Component) {
return (props) => {
const graphComponent = useMemo(() => {
const graphComponent2 = new GraphComponent();
graphComponent2.htmlElement.style.width = "100%";
graphComponent2.htmlElement.style.height = "100%";
graphComponent2.htmlElement.style.minWidth = "400px";
graphComponent2.htmlElement.style.minHeight = "400px";
graphComponent2.horizontalScrollBarPolicy = ScrollBarVisibility.AUTO;
graphComponent2.verticalScrollBarPolicy = ScrollBarVisibility.AUTO;
return graphComponent2;
}, []);
return /* @__PURE__ */ jsx(GraphComponentContext.Provider, { value: graphComponent, children: /* @__PURE__ */ jsx(Component, { ...props }) });
};
}
function useAddGraphComponent(parentRef, graphComponent) {
useLayoutEffect(() => {
if (parentRef.current) {
const firstChild = parentRef.current.firstChild;
if (firstChild) {
parentRef.current.insertBefore(graphComponent.htmlElement, firstChild);
} else {
parentRef.current.appendChild(graphComponent.htmlElement);
}
}
return () => {
if (parentRef.current) {
parentRef.current.removeChild(graphComponent.htmlElement);
}
};
}, []);
}
// src/context-menu/DefaultRenderMenu.tsx
import { jsx as jsx2 } from "react/jsx-runtime";
function DefaultRenderMenu({
item,
menuItems,
onClose
}) {
return menuItems.length > 0 ? menuItems.map((menuItem, i) => {
return /* @__PURE__ */ jsx2(
"button",
{
onClick: () => {
onClose();
menuItem.action(item);
},
className: "yfiles-react-context-menu__item",
children: menuItem.title
},
i
);
}) : null;
}
// src/context-menu/ContextMenu.tsx
import { Fragment, jsx as jsx3 } from "react/jsx-runtime";
function ContextMenu({
menuItems,
renderMenu,
extraProps
}) {
const [menuVisible, setMenuVisible] = useState(false);
const [menuLocation, setMenuLocation] = useState({ x: 0, y: 0 });
const [dataItem, setDataItem] = useState(null);
const graphComponent = useGraphComponent();
const populateContextMenu = useCallback(
(args) => {
const modelItem = args.item;
graphComponent.currentItem = modelItem;
const dataItem2 = modelItem?.tag;
setDataItem(dataItem2);
args.showMenu = true;
},
[graphComponent]
);
useLayoutEffect2(() => {
const closeMenuListener = () => {
setMenuVisible(false);
};
const inputMode = graphComponent.inputMode;
inputMode.contextMenuInputMode.addEventListener("menu-closed", closeMenuListener);
const populateContextMenuListener = (event) => {
if (event.item) {
graphComponent.selection.clear();
graphComponent.selection.add(event.item);
}
setMenuVisible(true);
setMenuLocation(graphComponent.worldToViewCoordinates(event.queryLocation));
populateContextMenu(event);
};
inputMode.addEventListener("populate-item-context-menu", populateContextMenuListener);
return () => {
inputMode.contextMenuInputMode.removeEventListener("menu-closed", closeMenuListener);
inputMode.removeEventListener("populate-item-context-menu", populateContextMenuListener);
};
}, [graphComponent, populateContextMenu]);
return /* @__PURE__ */ jsx3(Fragment, { children: menuVisible && /* @__PURE__ */ jsx3(
ContextMenuCore,
{
dataItem,
menuItems,
menuLocation,
onClose: () => {
setMenuVisible(false);
},
renderMenu,
extraProps
}
) });
}
function ContextMenuCore({
dataItem,
menuItems,
menuLocation,
onClose,
renderMenu = DefaultRenderMenu,
extraProps
}) {
const menu = createElement(renderMenu, {
menuItems: menuItems ? menuItems(dataItem) : [],
item: dataItem,
onClose,
...extraProps
});
return menu && /* @__PURE__ */ jsx3(
"div",
{
style: {
position: "absolute",
left: menuLocation.x,
top: menuLocation.y
},
className: "yfiles-react-context-menu",
children: menu
}
);
}
// src/controls/Controls.tsx
import { createElement as createElement2 } from "react";
// src/utils/combine-css-classes.ts
function combineCssClasses(classes) {
return classes.filter(Boolean).join(" ");
}
// src/controls/DefaultRenderControls.tsx
import { jsx as jsx4, jsxs } from "react/jsx-runtime";
function DefaultRenderControls({ buttons, orientation }) {
const separatorClass = combineCssClasses([
"yfiles-react-controls__separator",
`yfiles-react-controls__separator--${orientation}`
]);
const buttonContainerClass = combineCssClasses([
"yfiles-react-controls__button-container",
`yfiles-react-controls__button-container--${orientation}`
]);
return buttons.length > 0 ? buttons.map((button, i) => {
return /* @__PURE__ */ jsxs("div", { className: buttonContainerClass, children: [
/* @__PURE__ */ jsx4(
"button",
{
className: `yfiles-react-controls__button ${button.className || ""}`,
onClick: button.action,
disabled: button.disabled,
title: button.tooltip,
children: typeof button.icon === "string" ? /* @__PURE__ */ jsx4(
"img",
{
className: "yfiles-react-controls__button-img",
src: button.icon,
alt: button.tooltip
}
) : button.icon
},
`b-${i}`
),
i < buttons.length - 1 && /* @__PURE__ */ jsx4("div", { className: separatorClass })
] }, `sep-${i}`);
}) : null;
}
// src/controls/Controls.tsx
import { jsx as jsx5 } from "react/jsx-runtime";
function Controls({
buttons,
orientation = "vertical",
position = "top-right",
className,
renderControls = DefaultRenderControls
}) {
const toolbar = createElement2(renderControls, {
buttons: buttons(),
orientation,
position
});
const classes = [className, "yfiles-react-controls", `yfiles-react-controls--${orientation}`];
if (position !== "custom") {
classes.push(`yfiles-react-controls__positioned--${position}`);
classes.push("yfiles-react-controls__positioned");
}
const toolbarClassList = combineCssClasses(classes);
return /* @__PURE__ */ jsx5("div", { className: "yfiles-react-controls__toolbar-container", children: toolbar && /* @__PURE__ */ jsx5("div", { className: toolbarClassList, children: toolbar }) });
}
// src/controls/DefaultControlButtons.tsx
import { Command } from "@yfiles/yfiles";
function DefaultControlButtons() {
const graphComponent = useGraphComponent();
if (!graphComponent) {
return [];
}
const items = [];
items.push({
action: () => graphComponent.executeCommand(Command.INCREASE_ZOOM, null),
className: "yfiles-react-controls__button--zoom-in",
tooltip: "Increase zoom"
});
items.push({
action: () => graphComponent.executeCommand(Command.ZOOM, 1),
className: "yfiles-react-controls__button--zoom-original",
tooltip: "Zoom to original size"
});
items.push({
action: () => graphComponent.executeCommand(Command.DECREASE_ZOOM, null),
className: "yfiles-react-controls__button--zoom-out",
tooltip: "Decrease zoom"
});
items.push({
action: () => graphComponent.executeCommand(Command.FIT_GRAPH_BOUNDS, null),
className: "yfiles-react-controls__button--zoom-fit",
tooltip: "Fit content"
});
return items;
}
// src/graph-component/ReactComponentHtmlNodeStyle.ts
import { HtmlVisual, NodeStyleBase } from "@yfiles/yfiles";
import { memo } from "react";
var defaultTagProvider = (_, node) => node.tag;
var ReactComponentHtmlNodeStyle = class extends NodeStyleBase {
/**
* Creates a new instance.
* @param reactComponent The React component rendering the HTML content.
* @param setNodeInfos Callback for setting the node infos to be rendered.
* @param tagProvider The optional provider function that provides the "tag" in the props.
* By default, this will use the node's tag.
*/
constructor(reactComponent, setNodeInfos, tagProvider = defaultTagProvider) {
super();
this.setNodeInfos = setNodeInfos;
this.tagProvider = tagProvider;
const memoizedComponent = memo(reactComponent);
memoizedComponent.displayName = "ReactComponentHtmlNodeStyle-NodeTemplate";
this.component = memoizedComponent;
}
component;
createProps(context, node, cloneData) {
const graphComponent = context.canvasComponent;
const inputMode = graphComponent.inputMode;
return {
selected: graphComponent.selection.includes(node),
hovered: inputMode?.itemHoverInputMode.currentHoverItem === node,
focused: graphComponent.currentItem === node,
width: node.layout.width,
height: node.layout.height,
detail: context.zoom < 0.5 ? "low" : "high",
dataItem: cloneData ? { ...this.tagProvider(context, node) } : this.tagProvider(context, node)
};
}
createVisual(context, node) {
const props = this.createProps(context, node, true);
const div = document.createElement("div");
const visual = HtmlVisual.from(div);
this.setNodeInfos && this.setNodeInfos((nodeInfos) => {
const info = { domNode: div, props, component: this.component, node, visual };
const newInfos = nodeInfos.filter((info2) => info2.visual !== visual);
newInfos.push(info);
return newInfos;
});
HtmlVisual.setLayout(visual.element, node.layout);
context.setDisposeCallback(visual, () => {
this.setNodeInfos && this.setNodeInfos((nodeInfos) => {
return nodeInfos.filter((info) => info.visual !== visual);
});
return null;
});
return visual;
}
updateVisual(context, oldVisual, node) {
const newProps = this.createProps(context, node, false);
this.setNodeInfos && this.setNodeInfos((nodeInfos) => {
const oldInfo = nodeInfos.find((info) => info.visual === oldVisual);
if (!oldInfo) {
return nodeInfos;
}
const oldProps = oldInfo.props;
if (!this.areEqual(oldProps, newProps)) {
const newInfo = {
domNode: oldVisual.element,
props: this.createProps(context, node, true),
component: this.component,
visual: oldVisual,
node
};
const newInfos = nodeInfos.filter((info) => info !== oldInfo);
newInfos.push(newInfo);
return newInfos;
}
return nodeInfos;
});
HtmlVisual.setLayout(oldVisual.element, node.layout);
return oldVisual;
}
areEqual(oldProps, newProps) {
return oldProps.selected === newProps.selected && oldProps.hovered === newProps.hovered && oldProps.focused === newProps.focused && oldProps.detail === newProps.detail && oldProps.width === newProps.width && oldProps.height === newProps.height && this.deepEquals(oldProps.dataItem, newProps.dataItem);
}
deepEquals(obj1, obj2) {
if (obj1 === obj2) {
return true;
}
if (typeof obj1 !== "object" || typeof obj2 !== "object") {
return false;
}
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) {
return false;
}
for (const key of keys1) {
if (!keys2.includes(key)) {
return false;
}
if (!this.deepEquals(obj1[key], obj2[key])) {
return false;
}
}
return true;
}
};
// src/graph-component/ReactComponentHtmlGroupNodeStyle.ts
var ReactComponentHtmlGroupNodeStyle = class extends ReactComponentHtmlNodeStyle {
constructor(reactComponent, setNodeInfos, tagProvider = defaultTagProvider) {
super(reactComponent, setNodeInfos, tagProvider);
}
createProps(context, node, cloneData) {
const graphComponent = context.canvasComponent;
const foldingView = graphComponent.graph.foldingView;
return {
...super.createProps(context, node, cloneData),
isFolderNode: foldingView ? !foldingView.isExpanded(node) : false
};
}
areEqual(oldProps, newProps) {
return super.areEqual(oldProps, newProps) && oldProps.isFolderNode === newProps.isFolderNode;
}
};
// src/graph-component/withGraphComponent.tsx
import { useRef } from "react";
import { Fragment as Fragment2, jsx as jsx6 } from "react/jsx-runtime";
function withGraphComponent(Component) {
return (props) => {
const graphComponent = useGraphComponent();
const gcContainer = useRef(null);
if (graphComponent) {
useAddGraphComponent(gcContainer, graphComponent);
}
return /* @__PURE__ */ jsx6(Fragment2, { children: /* @__PURE__ */ jsx6(
"div",
{
ref: gcContainer,
className: props.className ?? "yfiles-react-graph-component-container",
style: props.style,
children: /* @__PURE__ */ jsx6(Component, { ...props })
}
) });
};
}
// src/graph-component/EdgeStyles.ts
import { Arrow as YArrow, ArrowType, PolylineEdgeStyle, Stroke } from "@yfiles/yfiles";
function convertToPolylineEdgeStyle(style) {
return new PolylineEdgeStyle({
smoothingLength: style.smoothingLength ?? 0,
stroke: new Stroke({
fill: style.className ? "currentColor" : "rgb(170, 170, 170)",
thickness: style.thickness ?? 1
}),
cssClass: style.className ?? "",
sourceArrow: convertArrow(style, true),
targetArrow: convertArrow(style)
});
}
function convertArrow(style, isSource = false) {
const arrow = isSource ? style.sourceArrow : style.targetArrow;
if (!arrow) {
return new YArrow(ArrowType.NONE);
}
const arrowColor = arrow.color ?? (style.className ? "currentColor" : "black");
return new YArrow({
stroke: `${(style.thickness ?? 1) * 0.5}px ${arrowColor}`,
fill: arrowColor,
type: arrow.type ?? "stealth"
});
}
// src/graph-search/GraphSearch.ts
import {
Color,
HighlightIndicatorManager,
INode,
Insets,
NodeStyleIndicatorRenderer,
Point,
Rect,
ShapeNodeStyle,
Stroke as Stroke2,
StyleIndicatorZoomPolicy
} from "@yfiles/yfiles";
var GraphSearch = class {
graphComponent;
searchHighlightIndicatorManager;
matchingNodes = [];
/**
* Registers event listeners at the search box.
*
* The search result is updated on every key press and the 'ENTER' key zooms the viewport to the currently
* matching nodes.
*
* @param searchBox The search box element.
* @param graphSearch The GraphSearch instance.
* @param autoCompleteSuggestions A list of possible auto-complete suggestion strings. If omitted, no auto-complete will be available
*/
static registerEventListener(searchBox, graphSearch, autoCompleteSuggestions) {
if (autoCompleteSuggestions && searchBox instanceof HTMLInputElement) {
const datalist = document.createElement("datalist");
datalist.id = searchBox.id + "-autocomplete";
searchBox.setAttribute("list", datalist.id);
if (searchBox.parentElement) {
searchBox.parentElement.insertBefore(datalist, searchBox);
}
graphSearch.updateAutoCompleteSuggestions(searchBox, autoCompleteSuggestions);
}
searchBox.addEventListener("input", async (e) => {
const input = e.target;
const searchText = input.value;
graphSearch.updateSearch(searchText);
if (!(e instanceof InputEvent) || e.inputType === "insertReplacementText") {
if (hasSelectedElementFromDatalist(input, searchText)) {
await graphSearch.zoomToSearchResult();
}
}
});
searchBox.addEventListener("keydown", async (e) => {
if (e.key === "Enter") {
e.preventDefault();
await graphSearch.zoomToSearchResult();
}
});
searchBox.addEventListener("keyup", (e) => {
if (e.key === "Enter") {
return;
}
});
}
/**
* Creates a new instance of this class with the default highlight style.
*
* @param graphComponent The graphComponent on which the search will be applied
*/
constructor(graphComponent) {
this.graphComponent = graphComponent;
const highlightColor = Color.TOMATO;
this.searchHighlightIndicatorManager = this.searchHighlightIndicatorManager = new SearchHighlightIndicatorManager({
nodeRenderer: new NodeStyleIndicatorRenderer({
nodeStyle: new ShapeNodeStyle({
stroke: new Stroke2(highlightColor.r, highlightColor.g, highlightColor.b, 220, 3),
fill: null
}),
margins: 3,
zoomPolicy: StyleIndicatorZoomPolicy.MIXED
}),
domain: graphComponent.highlightIndicatorManager.domain
});
this.searchHighlightIndicatorManager.install(graphComponent);
}
/**
* Gets the decoration style used for highlighting the matching nodes.
*/
get highlightRenderer() {
return this.searchHighlightIndicatorManager.nodeRenderer;
}
/**
* Sets the decoration style used for highlighting the matching nodes.
* @param highlightRenderer The given highlight style.
*/
set highlightStyle(highlightRenderer) {
this.searchHighlightIndicatorManager.nodeRenderer = highlightRenderer;
}
/**
* Updates the search results for the given search query.
* @param needle The data of the search query.
*/
updateSearch(needle) {
const highlights = this.searchHighlightIndicatorManager.items;
highlights.clear();
this.matchingNodes = [];
if (typeof needle === "string" && needle.trim() !== "") {
this.graphComponent.graph.nodes.filter((node) => this.matches(node, needle)).forEach((node) => {
highlights.add(node);
this.matchingNodes.push(node);
});
}
}
/**
* Updates the auto-complete list for the given search field with
* the given new suggestions.
*
* This will do nothing, unless auto-complete has been configured with initial suggestions
* in the {@link registerEventListener} call.
*
* @param input An HTML `input` element that is used as a search input.
* @param autoCompleteSuggestions A list of possible auto-complete suggestion strings.
*/
updateAutoCompleteSuggestions(input, autoCompleteSuggestions) {
const datalist = input.list;
if (!datalist) {
return;
}
while (datalist.lastChild) {
datalist.lastChild.remove();
}
for (const item of autoCompleteSuggestions) {
const option = document.createElement("option");
option.value = item;
datalist.appendChild(option);
}
}
/**
* Zooms to the nodes that match the result of the current search.
*/
zoomToSearchResult() {
if (this.matchingNodes.length === 0) {
return Promise.resolve();
}
const maxRect = this.matchingNodes.map((node) => node.layout.toRect()).reduce((prev, current) => Rect.add(prev, current));
if (!maxRect.isFinite) {
return Promise.resolve();
}
const rect = maxRect.getEnlarged(new Insets(20));
const componentWidth = this.graphComponent.size.width;
const componentHeight = this.graphComponent.size.height;
const maxPossibleZoom = Math.min(componentWidth / rect.width, componentHeight / rect.height);
const zoom = Math.min(maxPossibleZoom, 1.5);
return this.graphComponent.zoomToAnimated(zoom, new Point(rect.centerX, rect.centerY));
}
/**
* Specifies whether the given node is a match when searching for the given text.
*
* This implementation searches for the given string in the label text of the nodes.
* Overwrite this method to implement custom matching rules.
*
* @param node The node to be examined.
* @param needle The search data to be queried.
* @returns True if the node matches the text, false otherwise
*/
matches(node, needle) {
return typeof needle === "string" ? node.labels.some((label) => label.text.toLowerCase().indexOf(needle.toLowerCase()) !== -1) : false;
}
};
function hasSelectedElementFromDatalist(input, searchText) {
if (input.list) {
for (const option of Array.from(input.list.children)) {
if (option instanceof HTMLOptionElement && option.value === searchText) {
return true;
}
}
}
return false;
}
var SearchHighlightIndicatorManager = class extends HighlightIndicatorManager {
nodeRenderer;
constructor({
nodeRenderer,
domain
}) {
super();
this.nodeRenderer = nodeRenderer;
this.domain = domain;
}
getRenderer(item) {
if (item instanceof INode) {
return this.nodeRenderer;
}
return super.getRenderer(item);
}
};
// src/graph-search/useGraphSearch.ts
import { useCallback as useCallback2, useEffect, useMemo as useMemo2 } from "react";
function useGraphSearch(graphComponent, searchQuery, onSearch) {
const graphSearch = useMemo2(() => new NodeTagSearch(graphComponent, onSearch), [graphComponent]);
useEffect(() => {
graphSearch.onSearch = onSearch;
updateSearch();
}, [onSearch]);
const updateSearch = useCallback2(() => {
graphSearch.updateSearch(searchQuery);
}, [searchQuery, graphSearch]);
useEffect(() => {
graphComponent.graph.addEventListener("node-created", updateSearch);
graphComponent.graph.addEventListener("node-removed", updateSearch);
return () => {
graphComponent.graph.removeEventListener("node-created", updateSearch);
graphComponent.graph.removeEventListener("node-removed", updateSearch);
};
}, [graphComponent, searchQuery, updateSearch]);
useEffect(() => {
updateSearch();
}, [searchQuery, updateSearch]);
return graphSearch;
}
var NodeTagSearch = class extends GraphSearch {
constructor(graphComponent, onSearch) {
super(graphComponent);
this.onSearch = onSearch;
}
matches(node, needle) {
if (node.tag) {
const data = node.tag;
if (this.onSearch) {
return this.onSearch(data, needle);
}
return typeof needle === "string" && Object.keys(data).some((key) => {
const value = data[key];
if (typeof value === "string") {
return value.toLowerCase().indexOf(needle.toLowerCase()) !== -1;
}
return false;
});
}
return false;
}
};
// src/tooltip-component/Tooltip.tsx
import {
GraphItemTypes,
Point as Point2,
TimeSpan
} from "@yfiles/yfiles";
import { createElement as createElement3, useEffect as useEffect2, useState as useState2 } from "react";
// src/tooltip-component/DefaultRenderTooltip.tsx
import { jsx as jsx7 } from "react/jsx-runtime";
function DefaultRenderTooltip({ data }) {
return /* @__PURE__ */ jsx7("div", { className: "yfiles-react-tooltip", children: data.id });
}
// src/tooltip-component/Tooltip.tsx
import { createPortal } from "react-dom";
import { Fragment as Fragment3, jsx as jsx8 } from "react/jsx-runtime";
function Tooltip({ renderTooltip, extraProps }) {
const graphComponent = useGraphComponent();
const [tooltipRenderInfo, setTooltipRenderInfo] = useState2(
null
);
useEffect2(() => {
const inputMode = graphComponent.inputMode;
inputMode.toolTipItems = GraphItemTypes.NODE | GraphItemTypes.EDGE;
const mouseHoverInputMode = inputMode.toolTipInputMode;
mouseHoverInputMode.toolTipLocationOffset = new Point2(15, 15);
mouseHoverInputMode.delay = TimeSpan.fromMilliseconds(500);
mouseHoverInputMode.duration = TimeSpan.fromSeconds(5);
const queryItemTooltipListener = (evt) => {
if (evt.handled) {
return;
}
evt.toolTip = createTooltipContent(evt.item, setTooltipRenderInfo, renderTooltip);
evt.handled = true;
};
inputMode.addEventListener("query-item-tool-tip", queryItemTooltipListener);
return () => {
inputMode.removeEventListener("query-item-tool-tip", queryItemTooltipListener);
};
}, [graphComponent, renderTooltip]);
return /* @__PURE__ */ jsx8(Fragment3, { children: tooltipRenderInfo && createPortal(
createElement3(
TooltipWrapper,
{},
createElement3(tooltipRenderInfo.component, {
...tooltipRenderInfo.props,
...extraProps
})
),
tooltipRenderInfo.domNode
) });
}
function createTooltipContent(item, setTooltipInfo, renderTooltip) {
const tooltipContainer = document.createElement("div");
const template = renderTooltip ?? DefaultRenderTooltip;
setTooltipInfo({
domNode: tooltipContainer,
component: template,
props: { data: item.tag }
});
return tooltipContainer;
}
function TooltipWrapper({ children }) {
return /* @__PURE__ */ jsx8(Fragment3, { children });
}
// src/overview-component/Overview.tsx
import { useEffect as useEffect3, useRef as useRef2 } from "react";
import { GraphOverviewComponent } from "@yfiles/yfiles";
import { jsx as jsx9, jsxs as jsxs2 } from "react/jsx-runtime";
function Overview({ title = "Overview", className, position = "top-left" }) {
const graphComponent = useGraphComponent();
const overviewContainer = useRef2(null);
useEffect3(() => {
if (overviewContainer.current && graphComponent) {
const overview = new GraphOverviewComponent(overviewContainer.current, graphComponent);
return () => {
overview.cleanUp();
};
}
}, []);
const classes = [className, "yfiles-react-overview"];
if (position !== "custom") {
classes.push(`yfiles-react-overview__positioned--${position}`);
classes.push("yfiles-react-overview__positioned");
}
const overviewClassList = combineCssClasses(classes);
return /* @__PURE__ */ jsxs2("div", { className: overviewClassList, children: [
/* @__PURE__ */ jsx9("div", { className: "yfiles-react-overview__title", children: title }),
/* @__PURE__ */ jsx9("div", { className: "yfiles-react-overview__graph-component", ref: overviewContainer })
] });
}
// src/license/LicenseError.tsx
import { Fragment as Fragment4, jsx as jsx10, jsxs as jsxs3 } from "react/jsx-runtime";
function LicenseError(props) {
return /* @__PURE__ */ jsx10(Fragment4, { children: /* @__PURE__ */ jsx10("div", { className: "yfiles-react-license-error", children: /* @__PURE__ */ jsxs3("div", { className: "yfiles-react-license-error-dialog", children: [
/* @__PURE__ */ jsx10("div", { className: "yfiles-react-license-error-dialog__title", children: "Invalid / Missing License" }),
/* @__PURE__ */ jsxs3("div", { className: "yfiles-react-license-error-dialog__content", children: [
/* @__PURE__ */ jsxs3("div", { className: "yfiles-react-license-error-dialog__paragraph", children: [
"The ",
/* @__PURE__ */ jsx10("em", { children: props.componentName }),
" requires a valid",
" ",
/* @__PURE__ */ jsx10("a", { href: "https://www.yworks.com/products/yfiles-for-html", children: "yFiles for HTML" }),
" license."
] }),
/* @__PURE__ */ jsxs3("div", { className: "yfiles-react-license-error-dialog__paragraph", children: [
"You can evaluate yFiles for 60 days free of charge on",
" ",
/* @__PURE__ */ jsx10("a", { href: "https://my.yworks.com/signup?product=YFILES_HTML_EVAL", children: "my.yworks.com" }),
"."
] }),
/* @__PURE__ */ jsxs3("div", { className: "yfiles-react-license-error-dialog__paragraph", children: [
"Add the ",
/* @__PURE__ */ jsx10("code", { children: "license.json" }),
" to your React application and register it before using the component."
] }),
/* @__PURE__ */ jsx10("div", { className: "yfiles-react-license-error-dialog__code-snippet", children: /* @__PURE__ */ jsx10("pre", { children: props.codeSample }) })
] })
] }) }) });
}
// src/license/registerLicense.ts
import { Graph, License } from "@yfiles/yfiles";
// src/utils/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");
});
}
// src/utils/DevMode.ts
var isProd = false;
try {
isProd = process.env.NODE_ENV === "production";
} catch (e) {
}
// src/license/registerLicense.ts
function registerLicense(licenseKey) {
License.value = licenseKey;
setWebWorkerLicense(licenseKey);
}
function checkLicense() {
if (!isProd) {
const g = new Graph();
g.createNode();
return g.nodes.size === 1;
}
return true;
}
// src/timeline/Timeline.tsx
import { useCallback as useCallback3, useEffect as useEffect4, useState as useState3 } from "react";
import { createPortal as createPortal2 } from "react-dom";
// src/timeline/engine/TimelineEngine.ts
import {
Cursor,
delegate as delegate3,
FolderNodeDefaults,
FoldingManager,
GraphBuilder,
GraphComponent as GraphComponent3,
GraphItemTypes as GraphItemTypes2,
GraphViewerInputMode,
INode as INode2,
ItemHoverInputMode,
List,
MouseWheelBehaviors,
NodeStyleIndicatorRenderer as NodeStyleIndicatorRenderer3,
Point as Point5,
Rect as Rect5,
ScrollBarVisibility as ScrollBarVisibility2,
ShapeNodeStyle as ShapeNodeStyle3,
Size
} from "@yfiles/yfiles";
// src/timeline/engine/TimeframeRectangle.ts
import {
BaseClass,
delegate,
HandleInputMode,
HandlePositions,
HandlesRenderer,
IHitTestable,
IPositionHandler,
IVisualCreator,
MoveInputMode,
MultiplexingInputMode,
MutableRectangle,
ObservableCollection,
Point as Point3,
Rect as Rect2,
RectangleHandle,
RenderMode,
SvgVisual
} from "@yfiles/yfiles";
// src/timeline/engine/Styling.ts
import {
IGroupPaddingProvider,
Insets as Insets2,
LabelStyle,
NodeStyleIndicatorRenderer as NodeStyleIndicatorRenderer2,
ShapeNodeStyle as ShapeNodeStyle2,
StretchNodeLabelModel
} from "@yfiles/yfiles";
// src/timeline/engine/Utilities.ts
var MONTHS = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
];
function* days(start, end) {
const floor = new Date(start.getFullYear(), start.getMonth(), start.getDate());
const ceiling = new Date(end.getFullYear(), end.getMonth(), end.getDate() + 1);
for (let currentDate = new Date(floor); currentDate < ceiling; ) {
const start2 = new Date(currentDate);
currentDate.setDate(currentDate.getDate() + 1);
const end2 = new Date(currentDate);
yield [start2, end2, String(start2.getDate())];
}
}
function* weeks(start, end) {
const floor = new Date(start.getFullYear(), start.getMonth(), 1);
const ceiling = new Date(end.getFullYear(), end.getMonth() + 1, 1);
let week = 1;
for (let currentDate = new Date(floor); currentDate < ceiling; ) {
const start2 = new Date(currentDate);
const label = String(week);
currentDate.setDate(currentDate.getDate() + 7);
if (currentDate.getMonth() > start2.getMonth() || currentDate.getFullYear() > start2.getFullYear()) {
currentDate.setDate(1);
week = 0;
}
const end2 = new Date(currentDate);
yield [start2, end2, label];
week++;
}
}
function* months(start, end) {
const floor = new Date(start.getFullYear(), start.getMonth(), 1);
const ceiling = new Date(end.getFullYear(), end.getMonth() + 1, 1);
for (let currentDate = new Date(floor); currentDate < ceiling; ) {
const start2 = new Date(currentDate);
currentDate.setMonth(currentDate.getMonth() + 1);
const end2 = new Date(currentDate);
yield [start2, end2, MONTHS[start2.getMonth()]];
}
}
function* years(start, end) {
const floor = new Date(start.getFullYear(), 0, 1);
const ceiling = new Date(end.getFullYear() + 1, 0, 1);
for (let currentDate = new Date(floor); currentDate < ceiling; ) {
const start2 = new Date(currentDate);
currentDate.setFullYear(currentDate.getFullYear() + 1);
const end2 = new Date(currentDate);
yield [start2, end2, String(start2.getFullYear())];
}
}
function intervalsIntersect(start1, end1, start2, end2) {
return !(end1 <= start2 || start1 >= end2);
}
function timeframeEquals([start1, end1], [start2, end2]) {
return start1.getTime() === start2.getTime() && end1.getTime() === end2.getTime();
}
// src/timeline/engine/Styling.ts
var defaultStyling = {
timeframe: { fill: "#ffd70044", stroke: "#ffa50044" },
bars: { fill: "lightgrey", stroke: "transparent" },
inTimeframeBars: { fill: "grey", stroke: "transparent" },
barHover: { fill: "transparent", stroke: "slateblue" },
barSelect: { fill: "#00008b", stroke: "transparent" },
sectionSelect: { fill: "none", stroke: "#00008b" },
legend: {
even: { backgroundFill: "#b3cddb", textFill: "white", font: "bold 16px Arial" },
odd: { backgroundFill: "#4281a4", textFill: "white", font: "bold 16px Arial" }
}
};
var Styling = class {
constructor(graphComponent, style) {
this.graphComponent = graphComponent;
this.style = style;
const nodeDecorator = graphComponent.graph.decorator.nodes;
nodeDecorator.focusRenderer.hide();
nodeDecorator.highlightRenderer.hide();
nodeDecorator.selectionRenderer.addFactory(
(node) => new NodeStyleIndicatorRenderer2({
nodeStyle: graphComponent.graph.isGroupNode(node) ? new ShapeNodeStyle2(style.sectionSelect ?? defaultStyling.sectionSelect) : new ShapeNodeStyle2(style.barSelect ?? defaultStyling.barSelect),
zoomPolicy: "world-coordinates",
margins: 2
})
);
nodeDecorator.groupPaddingProvider.addConstant(
IGroupPaddingProvider.create(() => new Insets2(0, 0, 0, 20))
);
this.defaultStyle = new ShapeNodeStyle2({
stroke: `1px solid ${this.style.bars?.stroke ?? defaultStyling.bars?.stroke}`,
fill: this.style.bars?.fill ?? defaultStyling.bars?.fill
});
this.inTimeframeStyle = new ShapeNodeStyle2({
stroke: `1px solid ${this.style.inTimeframeBars?.stroke ?? defaultStyling.inTimeframeBars?.stroke}`,
fill: this.style.inTimeframeBars?.fill ?? defaultStyling.inTimeframeBars?.fill
});
this.groupStyle = new ShapeNodeStyle2({ fill: null, stroke: null });
this.groupStyleEven = new LabelStyle({
backgroundFill: this.style.legend?.even?.backgroundFill ?? defaultStyling.legend?.even?.backgroundFill,
backgroundStroke: null,
textFill: this.style.legend?.even?.textFill ?? defaultStyling.legend?.even?.textFill,
font: this.style.legend?.even?.font ?? defaultStyling.legend?.even?.font,
padding: 1,
horizontalTextAlignment: "center"
});
this.groupStyleOdd = this.groupStyleEven.clone();
this.groupStyleOdd.backgroundFill = this.style.legend?.odd?.backgroundFill ?? defaultStyling.legend.odd.backgroundFill;
this.groupStyleOdd.textFill = this.style.legend?.odd?.textFill ?? defaultStyling.legend.odd.textFill;
this.groupStyleOdd.font = this.style.legend?.odd?.font ?? defaultStyling.legend.odd.font;
}
defaultStyle;
inTimeframeStyle;
groupStyle;
groupStyleEven;
groupStyleOdd;
/**
* Applies the current styles to the current selection and highlight.
*/
updateStyles([startDate, endDate]) {
const graph = this.graphComponent.graph;
for (const node of graph.nodes) {
const bucket = node.tag;
if (graph.isGroupNode(node)) {
graph.setStyle(node, this.groupStyle);
const label = node.labels.at(0);
if (label) {
graph.setLabelLayoutParameter(label, StretchNodeLabelModel.BOTTOM);
const isEven = bucket.indexInLayer % 2 === 0;
const style = isEven ? this.groupStyleEven : this.groupStyleOdd;
graph.setStyle(label, style);
}
} else {
const isInTimeFrame = intervalsIntersect(bucket.start, bucket.end, startDate, endDate);
const style = isInTimeFrame ? this.inTimeframeStyle : this.defaultStyle;
graph.setStyle(node, style);
}
}
}
};
// src/timeline/engine/TimeframeRectangle.ts
var TimeframeRectangle = class {
constructor(graphComponent, style) {
this.graphComponent = graphComponent;
this.style = style;
this.visual = new RectangleVisual(this.rect, style);
const rectangle = this.rect;
this.handleInputMode = new HandleInputMode({
priority: 0,
handlesRenderer: new HandlesRenderer(RenderMode.SVG),
handles: new ObservableCollection([
new RectangleHandle(HandlePositions.RIGHT, rectangle),
new RectangleHandle(HandlePositions.LEFT, rectangle)
])
});
const onBoundsChanged = this.onBoundsChanged.bind(this);
this.handleInputMode.addEventListener("drag-finished", onBoundsChanged);
this.handleInputMode.addEventListener("dragging", onBoundsChanged);
this.positionHandler = new RectanglePositionHandler(rectangle);
this.moveInputMode = new MoveInputMode({
hitTestable: IHitTestable.create(
(context, location) => rectangle.contains(location, context.hitTestRadius + 3 / context.zoom)
),
positionHandler: this.positionHandler,
priority: 1
});
this.moveInputMode.addEventListener("drag-finished", onBoundsChanged);
this.moveInputMode.addEventListener("dragging", onBoundsChanged);
this.arm();
}
rect = new MutableRectangle(0, 0, 0, 0);
visual;
renderTreeElement = null;
handleInputMode;
moveInputMode;
positionHandler;
boundsChangedListener = null;
setBounds(bounds, silent = false) {
this.rect.setShape(bounds);
if (!silent) {
this.onBoundsChanged();
}
}
get bounds() {
return this.rect.toRect();
}
set limits(limits) {
this.positionHandler.limits = limits;
}
get limits() {
return this.positionHandler.limits;
}
setBoundsChangedListener(listener) {
this.boundsChangedListener = delegate.combine(this.boundsChangedListener, listener);
}
removeBoundsChangedListener(listener) {
this.boundsChangedListener = delegate.remove(this.boundsChangedListener, listener);
}
onBoundsChanged() {
this.boundsChangedListener?.(this.rect.toRect());
}
arm() {
this.renderTreeElement = this.graphComponent.renderTree.createElement(
this.graphComponent.renderTree.backgroundGroup,
this.visual
);
const inputMode = this.graphComponent.inputMode;
if (!(inputMode instanceof MultiplexingInputMode)) {
throw new Error("RectangleIndicator requires a MultiplexingInputMode");
}
inputMode.add(this.handleInputMode);
inputMode.add(this.moveInputMode);
}
cleanup() {
if (this.renderTreeElement) {
this.graphComponent.renderTree.remove(this.renderTreeElement);
this.renderTreeElement = null;
}
const inputMode = this.graphComponent.inputMode;
inputMode.remove(this.handleInputMode);
inputMode.remove(this.moveInputMode);
}
};
var RectangleVisual = class extends BaseClass(IVisualCreator) {
/**
* Creates a new instance of RectangleVisual.
*
* @param rectangle The rectangle that determines the bounds of this visual object.
* @param style The styling for the rectangle
*/
constructor(rectangle, style) {
super();
this.rectangle = rectangle;
this.style = style;
}
/**
* Creates the rectangle.
*/
createVisual(context) {
const svgNamespace = "http://www.w3.org/2000/svg";
const container = window.document.createElementNS(svgNamespace, "g");
const rect = window.document.createElementNS(svgNamespace, "rect");
rect.setAttribute("class", "time-frame-rect");
rect.setAttribute("x", "0");
rect.setAttribute("y", "0");
rect.setAttribute("width", String(this.rectangle.width));
rect.setAttribute("height", String(this.rectangle.height));
rect.setAttribute("stroke-width", "3");
rect.setAttribute("stroke", this.style?.stroke ?? defaultStyling.timeframe.stroke);
rect.setAttribute("stroke-opacity", "1");
rect.setAttribute("fill", this.style?.fill ?? defaultStyling.timeframe.fill);
rect.setAttribute("fill-opacity", "1");
container.appendChild(rect);
container.setAttribute("transform", `translate(${this.rectangle.x} ${this.rectangle.y})`);
const svgVisual = new SvgVisual(container);
svgVisual.cache = {
location: new Point3(this.rectangle.x, this.rectangle.y),
size: this.rectangle.toSize()
};
return svgVisual;
}
/**
* Updates the rectangle to improve performance.
*/
updateVisual(context, oldVisual) {
const svgVisual = oldVisual;
const container = svgVisual.svgElement;
const oldDataCache = svgVisual.cache;
const newDataCache = {
location: new Point3(this.rectangle.x, this.rectangle.y),
size: this.rectangle.toSize()
};
if (!newDataCache.size.equals(oldDataCache?.size)) {
container.firstElementChild.setAttribute("width", String(this.rectangle.width));
container.firstElementChild.setAttribute("height", String(this.rectangle.height));
}
if (!newDataCache.location.equals(oldDataCache?.location)) {
container.setAttribute("transform", `translate(${this.rectangle.x} ${this.rectangle.y})`);
}
svgVisual.cache = newDataCache;
return svgVisual;
}
};
var RectanglePositionHandler = class _RectanglePositionHandler extends BaseClass(IPositionHandler) {
constructor(rectangle, limits = Rect2.INFINITE) {
super();
this.rectangle = rectangle;
this.limits = limits;
}
initialPosition = new Point3(0, 0);
get location() {
return this.rectangle.topLeft;
}
initializeDrag(context) {
this.initialPosition = this.location.toPoint();
}
handleMove(context, originalLocation, newLocation) {
this.updatePosition(originalLocation, newLocation);
}
dragFinished(context, originalLocation, newLocation) {
this.updatePosition(originalLocation, newLocation);
}
cancelDrag(context, originalLocation) {
this.rectangle.setLocation(originalLocation);
}
updatePosition(originalLocation, newLocation) {
const delta = newLocation.subtract(originalLocation);
this.rectangle.setLocation(this.limit(this.initialPosition.add(delta)));
}
limit(newLocation) {
return new Point3(
_RectanglePositionHandler.limitNumber(
newLocation.x,
this.rectangle.width,
this.limits.x,
this.limits.maxX
),
_RectanglePositionHandler.limitNumber(
newLocation.y,
this.rectangle.height,
this.limits.y,
this.limits.maxY
)
);
}
static limitNumber(pos, length, limitMin, limitMax) {
if (pos < limitMin) {
return limitMin;
} else if (pos + length > limitMax) {
return limitMax - length;
}
return pos;
}
};
// src/timeline/engine/AggregationFolderNodeConverter.ts
import {
FolderNodeConverter,
Rect as Rect3
} from "@yfiles/yfiles";
var AggregationFolderNodeConverter = class extends FolderNodeConverter {
initializeFolderNodeLayout(state, foldingView, viewNode, masterNode) {
this.updateLayout(state, masterNode);
}
updateFolderNodeState(state, foldingView, viewNode, masterNode) {
super.updateFolderNodeState(state, foldingView, viewNode, masterNode);
this.updateLayout(state, masterNode);
}
updateLayout(state, masterNode) {
const aggregatedValue = masterNode.tag.aggregatedValue;
const width = this.folderNodeDefaults.size?.width ?? 20;
const height = Math.max(aggregatedValue, 1);
state.layout = new Rect3(
masterNode.layout.center.x - width / 2,
masterNode.layout.maxY - height,
width,
height
);
}
};
// src/timeline/engine/TimeframeAnimation.ts
import {
Animator,
delegate as delegate2,
Point as Point4
} from "@yfiles/yfiles";
var TimeframeAnimation = class {
/**
* Creates a new TimeframeAnimation
* @param timeframeRect The rectangle used in the {@link TimeframeRectangle}
* @param timelineComponent The graph component presenting the timeline
*/
constructor(timeframeRect, timelineComponent) {
this.timeframeRect = timeframeRect;
this.timelineComponent = timelineComponent;
}
animator = null;
// private startLocation: Point | null = null
timeframeListener = null;
animationEndedListener = null;
animating = false;
/**
* Moves the time frame rightwards along the timeline until it reaches the right border.
*/
playAnimation() {
if (!this.animating) {
this.animator = new Animator({ canvasComponent: this.timelineComponent, allowUserInteraction: true });
this.animating = true;
void this.animator.animate(() => {
const timeframe = this.timeframeRect;
const viewport = this.timelineComponent.viewport;
const maxX = this.timelineComponent.contentBounds.x + this.timelineComponent.contentBounds.width;
if (timeframe.x + timeframe.width >= maxX || !this.timelineComponent.inputMode.enabled) {
this.stopAnimation();
this.updateAnimationEndedListeners();
return;
}
timeframe.x += 1;
this.updateListeners(timeframe.toRect());
if (viewport.x + viewport.width < timeframe.x + timeframe.width * 0.5) {
this.timelineComponent.viewPoint = new Point4(timeframe.x, viewport.y);
}
}, Number.POSITIVE_INFINITY);
}
}
/**
* Stops moving the time frame.
*/
stopAnimation() {
if (this.animator !== null) {
this.animator.stop();
this.animator = null;
this.animating = false;
}
}
/**
* Adds the listener invoked when the time frame changes.
*/
addTimeframeListener(listener) {
this.timeframeListener = delegate2.combine(this.timeframeListener, listener);
}
/**
* Removes the listener invoked when the time frame changes.
*/
removeTimeframeListener(listener) {
this.timeframeListener = delegate2.remove(this.timeframeListener, listener);
}
/**
* Updates all listeners that are interested in an interval change.
*/
updateListeners(timeframe) {
this.timeframeListener?.(timeframe);
}
/**
* Adds the listener invoked when the animation stops due to reaching the right end of the timeline.
*/
addAnimationEndedListener(listener) {
this.animationEndedListener = delegate2.combine(this.animationEndedListener, listener);
}
/**
* Removes the listener invoked when the animation stops due to reaching the right end of the timeline.
*/
removeAnimationEndedListener(listener) {
this.animationEndedListener = delegate2.remove(this.animationEndedListener, listener);
}
/**
* Updates all listeners that are interested in the event when the animation stops due to reaching the right
* end of the timeline.
*/
updateAnimationEndedListeners() {
this.animationEndedListener?.();
}
};
// src/timeline/engine/bucket-aggregation.ts
function getBucket(node) {
return node.tag;
}
function getItemsFromBucket(bucketNode) {
return getLeaves(getBucket(bucketNode));
}
function aggregateBuckets(items, getTimeEntry, granularities) {
let currentLevel = collectLeafBuckets(items, getTimeEntry);
const allNonLeafBuckets = [];
let layer = 1;
for (const granularity of granularities) {
currentLevel = aggregateBucketsCore(currentLevel, granularity, allNonLeafBuckets, layer);
layer++;
}
return allNonLeafBuckets;
}
function aggregateBucketsCore(buckets, iterateTimeSlices, allBuckets, layer) {
if (buckets.length === 0) {
return [];
}
const minDate = buckets[0].start;
const maxDate = buckets[buckets.length - 1].end;
const newBuckets = [];
const activeBuckets = /* @__PURE__ */ new Set();
let bucketIndex = 0;
for (const [start, end, label] of iterateTimeSlices(minDate, maxDate)) {
const childBuckets = [];
for (; bucketIndex < buckets.length && buckets[bucketIndex].start < end; bucketIndex++) {
const entry = buckets[bucketIndex];
activeBuckets.add(entry);
}
for (const bucket2 of Array.from(activeBuckets)) {
if (start <= bucket2.start && bucket2.end <= end) {
childBuckets.push(bucket2);
} else {
activeBuckets.delete(bucket2);
}
}
const bucket = {
type: "group",
start,
end,
children: childBuckets,
label,
aggregatedValue: 1,
layer,
indexInLayer: -1
};
let aggregatedValue = 0;
bucket.children.forEach((child, i) => {
child.parent = bucket;
child.index = i;
aggregatedValue += child.aggregatedValue;
});
bucket.aggregatedValue = aggregatedValue;
newBuckets.push(bucket);
allBuckets.push(bucket);
}
for (let i = 0; i < newBuckets.length; i++) {
newBuckets[i].indexInLayer = i;
}
return newBuckets;
}
function createIntervalBuckets(item, start, end) {
const buckets = [];
let currentDate = new Date(start);
const intervalLabel = `${start.toDateString()} - ${end.toDateString()}`;
while (currentDate <= end) {
buckets.push(createLeafBucket(item, intervalLabel, currentDate, currentDate));
const nextDate = currentDate.setDate(currentDate.getDate() + 1);
currentDate = new Date(nextDate);
}
return buckets;
}
function createLeafBucket(item, label, start, end) {
return {
type: "leaf",
item,
start,
end,
label,
aggregatedValue: 1,
layer: 0,
indexInLayer: -1
};
}
function collectLeafBuckets(items, getTimeRange) {
const entries = items.flatMap((item) => {
const timeRange = getTimeRange(item);
if