@bpgraph/core
Version:
**bpgraph** is a lightweight node-based visual programming library. It allows you to easily build interactive flow editors for low-code platforms, AI pipelines, or data processing systems.
1,408 lines (1,401 loc) • 127 kB
JavaScript
import { Element, ElementView, Graph, Link, Link$1, Listener, Paper, Rect, V_default, config, curve, defaultsDeep, left, mask, right, standard_exports, svg } from "./vendor-CHy9MZSZ.js";
//#region src/adapters/joint/Util.ts
function initCharWidth(fontSize, fontFamily = "system-ui, Avenir, Helvetica, Arial, sans-serif", charWidthCache) {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
context.font = `${fontSize}px ${fontFamily}`;
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ ";
for (const char of chars) charWidthCache[char] = context.measureText(char).width;
}
function getCharWidthCache(fontSize) {
const fontSizeCache = {};
if (!fontSizeCache[fontSize]) {
fontSizeCache[fontSize] = { default: fontSize * .5 };
initCharWidth(fontSize, "system-ui, Avenir, Helvetica, Arial, sans-serif", fontSizeCache[fontSize]);
}
return fontSizeCache[fontSize];
}
function getTextWidth(text, fontSize) {
let width = 0;
const charWidthCache = getCharWidthCache(fontSize);
for (const char of text) if (/[\u4e00-\u9fa5]/.test(char)) width += fontSize;
else width += charWidthCache[char] ?? charWidthCache.default;
return width;
}
function css(strings, ...values) {
const result = strings.reduce((acc, str, i) => acc + str + (values[i] ?? ""), "");
return result.replace(/\s+/g, " ").trim();
}
//#endregion
//#region src/adapters/joint/nodes/BlueprintNode.ts
var BlueprintNode = class extends Element {
constructor(attributes = {}, options = {}) {
const style = attributes.style || {};
const nodeType = attributes.nodeType;
const markup = [{
tagName: "rect",
groupSelector: "base-node-body"
}, {
tagName: "g",
groupSelector: "base-node-title"
}];
if (nodeType === "switch") markup.push({
tagName: "g",
groupSelector: "switch-plus",
children: [{
tagName: "rect",
groupSelector: "switch-plus-rect"
}, {
tagName: "path",
groupSelector: "switch-plus-icon"
}]
});
const rowHeight = style.ports?.layout?.rowHeight ?? 20;
const rowSpacing = style.ports?.layout?.gap ?? 8;
const portStroke = style.ports?.output?.port?.stroke ?? "rgba(255, 255, 255, 0.8)";
super(defaultsDeep(attributes, {
type: "custom.BlueprintNode",
size: {
width: 300,
height: 100
},
inputs: [],
outputs: [],
attrs: {
"base-node-body": {
refWidth: "100%",
refHeight: "100%",
fill: style.background ?? "rgba(41, 44, 47, 0.9)",
stroke: style.background ?? "rgba(41, 44, 47, 0.9)",
strokeWidth: 1,
rx: style.borderRadius ?? 8,
ry: style.borderRadius ?? 8
},
"switch-plus": {
cursor: "pointer",
refX: "100%",
refY: "100%",
transform: "translate(-22, -" + (rowSpacing + rowHeight * .5) + ")"
},
"switch-plus-rect": {
width: 12,
height: 12,
fill: "transparent",
stroke: "transparent",
strokeWidth: 1
},
"switch-plus-icon": {
d: "M2 6 L10 6 M6 2 L6 10",
stroke: portStroke,
strokeWidth: 1.5,
pointerEvents: "none"
}
},
markup,
ports: { groups: {
in: {
position: {
name: "absolute",
args: { x: 16 }
},
attrs: {
portBody: {
fill: style.ports?.input?.port?.fill ?? "rgba(255, 255, 255, 0.5)",
stroke: style.ports?.input?.port?.stroke ?? "rgba(255, 255, 255, 0.8)",
strokeWidth: style.ports?.input?.port?.strokeWidth ?? 1,
r: 6,
magnet: "inout",
cursor: "crosshair"
},
portLabel: {
x: 12,
y: "0.3em",
textAnchor: "start",
fontSize: style.ports?.input?.label?.fontSize ?? 12,
fill: style.ports?.input?.label?.color ?? "#fff"
}
}
},
out: {
position: {
name: "absolute",
args: { x: "calc(w - 16)" }
},
attrs: {
portBody: {
fill: style.ports?.output?.port?.fill ?? "rgba(255, 255, 255, 0.5)",
stroke: style.ports?.output?.port?.stroke ?? "rgba(255, 255, 255, 0.8)",
strokeWidth: style.ports?.output?.port?.strokeWidth ?? 1,
r: 6,
magnet: "inout",
cursor: "crosshair"
},
portLabel: {
x: -12,
y: "0.3em",
textAnchor: "end",
fontSize: style.ports?.output?.label?.fontSize ?? 12,
fill: style.ports?.output?.label?.color ?? "#fff"
}
}
}
} }
}), options);
}
initialize(...args) {
super.initialize(...args);
if (this.getPorts().length === 0) this.buildPortItems();
this.on("change:inputs change:outputs", (_el, _changed, opt) => this.buildPortItems(opt));
}
title(value) {
this.set("title", value);
const titleWidth = value.length > 0 ? getTextWidth(value, this.get("style")?.header?.fontSize || 12) : 0;
const size = this.get("size");
const width = size?.width || 0;
if (titleWidth + 35 > width) {
this.resize(titleWidth + 35, size?.height || 100);
return;
}
this.updateSizeByContent();
}
setStyle(style) {
this.set("style", style);
}
setValues(values) {
this.set("values", values);
}
buildPortItems(opt = {}) {
const items = [];
const inputs = this.get("inputs") || [];
const outputs = this.get("outputs") || [];
const style = this.get("style") || {};
const HEADER_HEIGHT = style.header?.height ?? 24;
const portHeight = style.ports?.layout?.rowHeight ?? 20;
const portSpacing = style.ports?.layout?.gap ?? 8;
const portTop = style.ports?.layout?.top ?? 0;
for (const [index, input] of inputs.entries()) {
if (input.type === "spacer") continue;
items.push({
...this.buildPortItem(input, "in"),
id: input.id,
group: "in",
args: { y: HEADER_HEIGHT + index * portHeight + (index + 1) * portSpacing + portHeight / 2 + portTop }
});
}
for (const [index, output] of outputs.entries()) {
if (output.type === "spacer") continue;
items.push({
...this.buildPortItem(output, "out"),
id: output.id,
group: "out",
args: { y: HEADER_HEIGHT + index * portHeight + (index + 1) * portSpacing + portHeight / 2 + portTop }
});
}
this.startBatch("update-ports");
this.prop(["ports", "items"], items, {
...opt,
rewrite: true
});
this.stopBatch("update-ports");
this.updateSizeByContent();
}
buildPortItem(port, group) {
const style = this.get("style") || {};
const portHeight = style.ports?.layout?.rowHeight ?? 20;
const left$1 = group === "in";
const portStyle = left$1 ? style.ports?.input : style.ports?.output;
const fontSize = portStyle?.label?.fontSize ?? 12;
const color = portStyle?.label?.color ?? "#fff";
const portLabelX = this.get("ports")?.groups?.in.attrs?.portLabel?.x || 12;
const portLabelWidth = left$1 ? getTextWidth(port.label || "", fontSize) : 0;
let showPort = port.showPort !== false;
if (port.type === "select" && port.showPort !== true) showPort = false;
const item = {
height: portHeight,
attrs: {
portLabel: { text: port.label || "" },
portForm: {
x: portLabelX + portLabelWidth + 4,
y: -portHeight / 2,
width: style.ports?.input?.editor?.box?.width || 80,
height: portHeight
},
portFormSvg: { transform: `translate(${portLabelX + portLabelWidth + 4}, ${-portHeight / 2})` },
portFormWrap: { style: {
textAlign: left$1 ? "left" : "right",
fontSize: fontSize + "px",
color
} },
field: { name: port.name }
}
};
const formWrap = left$1 ? this.renderFormWrap(port) : "";
item.markup = svg`
${showPort ? getPortIcon(port.type) : ""}
<text @selector='portLabel' />
${formWrap ? formWrap : ""}
`;
return item;
}
renderFormWrap(input) {
const showInput = input.showInput !== false;
if (!showInput) return "";
const style = this.get("style") || {};
const values = this.get("values") || {};
switch (input.type) {
case "string": return renderString(input, style, values);
case "number": return renderNumber(input, style, values);
case "select": return renderSelect(input, style, values);
case "boolean": return renderCheckbox(input, style, values);
default: return "";
}
}
updateSizeByContent() {
const ports = this.getPorts();
const style = this.get("style") || {};
const title = this.get("title") || "";
const nodeType = this.get("nodeType") || "";
const headerFontSize = style.header?.fontSize ?? 12;
const inputPorts = ports.filter((port) => port.group === "in");
const outputPorts = ports.filter((port) => port.group === "out");
const portCount = inputPorts.length > outputPorts.length ? inputPorts.length : outputPorts.length;
const titleHeight = style.header?.height ?? 24;
const portHeight = style.ports?.layout?.rowHeight ?? 20;
const portGap = style.ports?.layout?.gap ?? 8;
const paddingTop = style.ports?.layout?.top ?? 0;
const paddingBottom = style.ports?.layout?.bottom ?? 20;
const editorBoxWidth = style.ports?.input?.editor?.box?.width || 0;
let portsHeight = portCount > 0 ? portCount * portHeight + (portCount + 1) * portGap : 0;
if (nodeType === "switch") portsHeight += portHeight * .3 + portGap;
const titleWidth = title.length > 0 ? getTextWidth(title, headerFontSize) : 0;
const inputsWidth = inputPorts.length > 0 ? Math.max(...inputPorts.map((port) => {
const label = port.attrs?.portLabel?.text ?? "";
const markup = port.markup || [];
const fontSize = style.ports?.input?.label?.fontSize || 12;
const isPortForm = markup.some((m) => m.selector === "portForm");
const inputWidth = isPortForm ? editorBoxWidth + 8 : 0;
const labelWidth = label.length > 0 ? getTextWidth(label, fontSize) : 0;
return labelWidth + 25 + inputWidth;
})) : 0;
const outputsWidth = outputPorts.length > 0 ? Math.max(...outputPorts.map((port) => {
const label = port.attrs?.portLabel?.text ?? "";
const fontSize = style.ports?.output?.label?.fontSize || 12;
const labelWidth = label.length > 0 ? getTextWidth(label, fontSize) : 0;
return labelWidth + 25;
})) : 0;
const minWidth = 120;
const minHeight = 60;
const height = Math.max(minHeight, titleHeight + portsHeight + paddingTop + paddingBottom);
const width = Math.max(minWidth, titleWidth + 35, inputsWidth + outputsWidth + 14);
this.resize(width, height);
}
};
function getPortIcon(type) {
switch (type) {
case "exec": return `
<path @selector='portBody' d="${squareArrowPath(12, 12)}" />
<title>${type}</title>`;
default: return `<circle @selector="portBody" r="6" />
<title>${type}</title>`;
}
}
function squareArrowPath(w, h) {
const param = Math.max(w * .4, 0);
const offsetX = w / 2;
const offsetY = h / 2;
return [
`M${0 - offsetX},${0 - offsetY}`,
`L${w - param - offsetX},${0 - offsetY}`,
`L${w - offsetX},${h / 2 - offsetY}`,
`L${w - param - offsetX},${h - offsetY}`,
`L${0 - offsetX},${h - offsetY} Z`
].join(" ");
}
function renderString(input, style, values) {
const boxStyle = style.ports?.input?.editor?.box;
const portHeight = style.ports?.layout?.rowHeight ?? 20;
const inputStyle = css`
outline: none;
padding-left: 4px;
padding-right: 4px;
box-sizing: border-box;
width: 100%;
height: 22px;
line-height: 22px;
border: none;
background: ${boxStyle?.background ?? "transparent"};
color: ${boxStyle?.color ?? "#fff"};
font-size: ${boxStyle?.fontSize ?? 12}px;
border: 1px solid ${boxStyle?.borderColor ?? "rgba(255, 255, 255, 0.2)"};
border-radius: ${boxStyle?.borderRadius ?? 4}px;
`;
return `
<foreignObject @selector="portForm">
<div @selector="portFormWrap"
xmlns="http://www.w3.org/1999/xhtml"
style="width:100%; height:100%;line-height: ${portHeight - 2}px;">
<input @selector="field"
xmlns="http://www.w3.org/1999/xhtml"
type="text"
style="${inputStyle}"
value="${values[input.name] || ""}"
/>
</div>
</foreignObject>
`;
}
function renderNumber(input, style, values) {
const boxStyle = style.ports?.input?.editor?.box;
const portHeight = style.ports?.layout?.rowHeight ?? 20;
const inputStyle = css`
outline: none;
padding-left: 4px;
padding-right: 4px;
box-sizing: border-box;
width: 100%;
height: 22px;
line-height: 22px;
border: none;
background: ${boxStyle?.background ?? "transparent"};
color: ${boxStyle?.color ?? "#fff"};
font-size: ${boxStyle?.fontSize ?? 12}px;
border: 1px solid ${boxStyle?.borderColor ?? "rgba(255, 255, 255, 0.2)"};
border-radius: ${boxStyle?.borderRadius ?? 4}px;
`;
const upArrowPath = "M2 8 L6 4 L10 8";
const downArrowPath = "M2 3 L6 7 L10 3";
return `
<foreignObject @selector="portForm">
<div @selector="portFormWrap"
xmlns="http://www.w3.org/1999/xhtml"
style="width:100%; height:100%;line-height: ${portHeight - 2}px;">
<input @selector="field"
xmlns="http://www.w3.org/1999/xhtml"
type="text"
style="${inputStyle}"
value="${values[input.name] || ""}"
/>
<svg width="12" height="20" style="position:absolute; right:2px; top:0;" viewBox="0 0 12 20">
<rect width="12" height="11" y="0" fill="transparent" cursor="pointer" @selector="upArrow" />
<rect width="12" height="11" y="11" fill="transparent" cursor="pointer" @selector="downArrow" />
<path d="${upArrowPath}" stroke="currentColor" stroke-width="1.2" fill="transparent" pointer-events="none" />
<path d="${downArrowPath}" stroke="currentColor" stroke-width="1.2" fill="transparent" pointer-events="none" transform="translate(0,11)" />
</svg>
</div>
</foreignObject>
`;
}
function renderSelect(input, style, values) {
const boxStyle = style.ports?.input?.editor?.box;
const portHeight = style.ports?.layout?.rowHeight ?? 20;
const inputStyle = css`
outline: none;
padding-left: 4px;
padding-right: 16px;
box-sizing: border-box;
width: 100%;
height: 22px;
line-height: 22px;
border: none;
background: ${boxStyle?.background ?? "transparent"};
color: ${boxStyle?.color ?? "#fff"};
font-size: ${boxStyle?.fontSize ?? 12}px;
border: 1px solid ${boxStyle?.borderColor ?? "rgba(255, 255, 255, 0.2)"};
border-radius: ${boxStyle?.borderRadius ?? 4}px;
cursor: pointer;
`;
const value = values[input.name];
const options = input._options || [];
const label = options.find((opt) => opt.value === value)?.label || "";
return `
<foreignObject @selector="portForm">
<div @selector="portFormWrap"
xmlns="http://www.w3.org/1999/xhtml"
style="width:100%; height:100%;line-height: ${portHeight - 2}px;">
<input @selector="field"
xmlns="http://www.w3.org/1999/xhtml"
type="text"
title="${label || ""}"
style="${inputStyle}"
value="${label || ""}"
/>
<svg width="12" height="12" style="position:absolute; right:4px; top:6px; pointer-events:none;" viewBox="0 0 12 12">
<path d="M2 4 L6 8 L10 4" stroke="currentColor" stroke-width="1.5" fill="transparent" />
</svg>
</div>
</foreignObject>
`;
}
function renderCheckbox(input, style, values) {
return `
<g @selector="portFormSvg">
<rect
@selector="field"
xmlns="http://www.w3.org/2000/svg"
x="0" y="4"
width="16" height="16"
rx="${style.ports?.input?.editor?.box?.borderRadius ?? 3}"
ry="${style.ports?.input?.editor?.box?.borderRadius ?? 3}"
fill="${values[input.name] ? style.ports?.input?.editor?.box?.background ?? "#ff6666" : "transparent"}"
stroke="${style.ports?.input?.editor?.box?.borderColor ?? "rgba(255, 255, 255, 0.2)"}"
stroke-width="1"
cursor="pointer"
/>
<polyline
points="4,11 7,14 12,8"
fill="none"
cursor="pointer"
stroke="${style.ports?.input?.editor?.box?.color ?? "#fff"}"
stroke-width="1"
visibility="${values[input.name] ? "visible" : "hidden"}"
/>
</g>
`;
}
//#endregion
//#region src/adapters/joint/nodes/BlueprintNodeView.ts
var BlueprintNodeView = class extends ElementView {
highlightedPorts = /* @__PURE__ */ new Set();
dropdownElMap = /* @__PURE__ */ new Map();
currentlyOpenDropdown = null;
initialize(...args) {
this.paper?.el.addEventListener("click", this.hideDropdown);
super.initialize(...args);
this.listenTo(this.model, "change:title", this.refreshTitle);
this.listenTo(this.model, "change:inputs change:outputs", this.applyPortHighlights);
}
onRender() {
this.paper?.el.removeEventListener("click", this.otherClick);
this.paper?.el.addEventListener("click", this.otherClick);
const inputs = this.model.get("inputs") || [];
inputs.forEach((input) => {
const inputEl = this.findPortNode(input.id, "field");
if (!inputEl) return;
if (input.type === "boolean") inputEl.addEventListener("click", this.updateFieldValue(input.type));
else if (input.type === "select") {
this.createDropdown(input.id, input.name, input._options);
inputEl.addEventListener("focus", this.showDropdown);
inputEl.addEventListener("input", this.filterDropdown);
} else if (input.type === "number") {
inputEl.addEventListener("input", this.validateNumber(false));
inputEl.addEventListener("blur", this.validateNumber(true));
const upArrow = this.findPortNode(input.id, "upArrow");
const downArrow = this.findPortNode(input.id, "downArrow");
upArrow?.addEventListener("click", () => {
const fieldEl = this.findPortNode(input.id, "field");
fieldEl.value = String(Number(fieldEl.value || 0) + 1);
fieldEl.dispatchEvent(new Event("blur"));
});
downArrow?.addEventListener("click", () => {
const fieldEl = this.findPortNode(input.id, "field");
fieldEl.value = String(Number(fieldEl.value || 0) - 1);
fieldEl.dispatchEvent(new Event("blur"));
});
inputEl.addEventListener("input", this.updateFieldValue(input.type));
} else if (input.type === "string") inputEl.addEventListener("input", this.updateFieldValue(input.type));
});
const plus = this.findNode("switch-plus-rect");
if (plus) plus.addEventListener("click", (evt) => {
evt.stopPropagation();
this.model.graph.trigger("node:switch:add-case", this.model);
});
}
otherClick = (evt) => {
const target = evt.target;
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.getAttribute("role") === "option") return;
if (this.currentlyOpenDropdown) this.hideDropdown();
};
createDropdown(inputId, inputName, options) {
const style = this.model.get("style") || {};
const dropdownStyle = style.ports?.input?.editor?.dropdown || {};
this.dropdownElMap.delete(inputName);
if (!options || options.length === 0) return;
const dropdownElement = document.createElement("div");
dropdownElement.addEventListener("wheel", (evt) => {
evt.stopPropagation();
evt.preventDefault();
});
dropdownElement.classList.add("bpgraph-node-dropdown");
dropdownElement.style.cssText = `
background: ${dropdownStyle.background || "#222"};
border: 1px solid ${dropdownStyle.borderColor || "rgba(120, 130, 140, 0.3)"};
color: ${dropdownStyle.color || "#fff"};
border-radius: ${dropdownStyle.borderRadius || 4}px;
z-index: 9999;
font-size: 12px;
max-height: 200px;
overflow: auto;
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
position: absolute;
`;
options.forEach((option) => {
const optionEl = document.createElement("div");
optionEl.classList.add("bpgraph-node-dropdown-option");
optionEl.setAttribute("style", `
padding: 2px 4px;
cursor: pointer;
`);
optionEl.setAttribute("data-value", option.value);
optionEl.textContent = option.label;
optionEl.addEventListener("click", (evt) => {
evt.preventDefault();
evt.stopPropagation();
this.setInputValue(inputName, evt.target.dataset.value);
this.findPortNode(inputId);
this.hideDropdown();
const fieldEl = this.findPortNode(inputId, "field");
if (fieldEl) fieldEl.value = evt.target.textContent || "";
fieldEl.title = String(fieldEl.value);
});
dropdownElement.appendChild(optionEl);
});
this.dropdownElMap.set(inputName, dropdownElement);
}
updateOptions(inputId, inputName, options) {
const dropdownElement = this.dropdownElMap.get(inputName);
if (!dropdownElement) return;
dropdownElement.innerHTML = "";
options.forEach((option) => {
const optionEl = document.createElement("div");
optionEl.classList.add("bpgraph-node-dropdown-option");
optionEl.setAttribute("style", `
padding: 2px 4px;
cursor: pointer;
`);
optionEl.setAttribute("data-value", option.value);
optionEl.textContent = option.label;
optionEl.addEventListener("click", (evt) => {
evt.preventDefault();
evt.stopPropagation();
this.setInputValue(inputName, evt.target.dataset.value);
this.findPortNode(inputId);
this.hideDropdown();
const fieldEl = this.findPortNode(inputId, "field");
if (fieldEl) fieldEl.value = evt.target.textContent || "";
fieldEl.title = String(fieldEl.value);
});
dropdownElement.appendChild(optionEl);
});
}
showDropdown = (evt) => {
if (this.currentlyOpenDropdown) this.hideDropdown();
const fieldEl = evt.target;
const portName = fieldEl.getAttribute("name");
if (!portName) return;
const dropdownEl = this.dropdownElMap.get(portName);
if (!dropdownEl) return;
const rect = fieldEl.getBoundingClientRect();
dropdownEl.style.minWidth = `${rect.width}px`;
dropdownEl.style.top = `${rect.bottom + 4}px`;
dropdownEl.style.left = `${rect.left}px`;
this.currentlyOpenDropdown = portName;
this.paper?.el.appendChild(dropdownEl);
};
hideDropdown = () => {
const portName = this.currentlyOpenDropdown;
if (document.activeElement && document.activeElement.getAttribute("name") === portName) return;
const dropdownEl = this.dropdownElMap.get(portName || "");
this.currentlyOpenDropdown = null;
if (!dropdownEl) return;
this.paper?.el.removeChild(dropdownEl);
};
filterDropdown = (evt) => {
const fieldEl = evt.target;
const portName = fieldEl.getAttribute("name");
if (!portName) return;
const dropdownEl = this.dropdownElMap.get(portName);
if (!dropdownEl) return;
const filter = fieldEl.value.toLowerCase();
const pattern = filter.split("").map((c) => c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
const regex = new RegExp(pattern, "i");
Array.from(dropdownEl.children).forEach((child) => {
const optionEl = child;
const label = optionEl.textContent || "";
if (regex.test(label)) optionEl.style.display = "block";
else optionEl.style.display = "none";
});
};
renderMarkup() {
super.renderMarkup();
const baseTitle = this.findNode("base-node-title");
if (baseTitle) renderTitle(baseTitle, this.model);
return this;
}
validateNumber(endInput = false) {
return (evt) => {
const fieldEl = evt.target;
const value = fieldEl.value;
if (endInput) {
if (value) {
const parsedValue = Number(fieldEl.value);
if (isNaN(parsedValue)) fieldEl.style.borderColor = "red";
else {
const style = this.model.get("style") || {};
const boxStyle = style.ports?.input?.editor?.box || {};
fieldEl.style.borderColor = boxStyle.borderColor || "";
}
}
} else if (value) {
const filtered = value.replace(/[^\d.-]/g, "");
if (filtered !== value) fieldEl.value = filtered;
}
};
}
updateFieldValue(type) {
return (evt) => {
const fieldEl = evt.target;
const values = this.model.get("values") || {};
const portName = fieldEl.getAttribute("name");
if (!portName) return;
let value = fieldEl.value;
if (type === "number") value = fieldEl.value ? Number(fieldEl.value) : NaN;
else if (type === "boolean") value = !values[portName];
fieldEl.title = String(value);
this.setInputValue(portName, value);
};
}
setInputValue(inputName, value) {
this.model.set("values", {
...this.model.get("values"),
[inputName]: value
});
this.model.graph.trigger("node:values:changed", this, inputName, value);
}
applyPortHighlights() {
requestAnimationFrame(() => {
setTimeout(() => {
this._applyPortHighlights();
}, 0);
});
}
_applyPortHighlights() {
const ports = this.model.getPorts();
const portIds = ports.map((port) => port.id);
const toRemove = [];
for (const portId of this.highlightedPorts) {
if (!portIds.includes(portId)) {
toRemove.push(portId);
continue;
}
this.highlightPort(portId);
}
for (const portId of toRemove) {
this.highlightedPorts.delete(portId);
this.unhighlightPort(portId);
}
}
refreshTitle() {
const baseTitle = this.findNode("base-node-title");
if (baseTitle) {
const title = this.model.get("title") || "";
const text = baseTitle.querySelector("text");
if (text) text.textContent = title;
}
}
refreshDomValue(name, step = 0) {
const values = this.model.get("values") || {};
const inputs = this.model.get("inputs") || [];
const input = inputs.find((i) => i.name === name);
if (!input) return;
const inputEl = this.findPortNode(input.id, "field");
if (step < 10) requestAnimationFrame(() => {
setTimeout(() => {
this.refreshDomValue(name, step + 1);
}, 0);
});
if (!inputEl) return;
let value = values[input.name];
switch (input.type) {
case "string":
if (typeof value !== "string") return;
break;
case "number":
if (typeof value !== "number") return;
value = isNaN(value) ? inputEl.value : String(value);
break;
case "boolean":
if (inputEl instanceof SVGElement) {
const polyline = inputEl.parentNode?.children[1];
if (!polyline) return;
polyline.setAttribute("visibility", value ? "visible" : "hidden");
return;
}
return;
case "select":
if (typeof value !== "string") return;
value = input._options?.find((option) => option.value === value)?.label || "";
break;
}
if (inputEl.value !== value) {
inputEl.value = value;
inputEl.title = String(value);
}
}
highlightPort(portId, retry = 0) {
if (!this.highlightedPorts.has(portId)) this.highlightedPorts.add(portId);
const portNode = this.findPortNode(portId, "portBody");
if (portNode) {
const style = this.model.get("style") || {};
const portStyle = (portId.startsWith("in-") ? style.ports?.input : style.ports?.output) || {};
const fill = portStyle.port?.highlightFill || "rgba(255, 255, 255, 0.8)";
const stroke = portStyle.port?.highlightStroke || "rgba(255, 255, 255, 1)";
const strokeWidth = portStyle.port?.highlightStrokeWidth ?? 1;
portNode.setAttribute("fill", fill);
portNode.setAttribute("stroke", stroke);
portNode.setAttribute("stroke-width", String(strokeWidth));
} else if (retry < 10) requestAnimationFrame(() => {
setTimeout(() => {
this.highlightPort(portId, retry + 1);
}, 0);
});
}
unhighlightPort(portId) {
const portNode = this.findPortNode(portId, "portBody");
if (portNode) {
const style = this.model.get("style") || {};
const portStyle = (portId.startsWith("in-") ? style.ports?.input : style.ports?.output) || {};
const fill = portStyle.port?.fill || "rgba(255, 255, 255, 0.5)";
const stroke = portStyle.port?.stroke || "rgba(255, 255, 255, 0.8)";
const strokeWidth = portStyle.port?.strokeWidth ?? 1;
portNode.setAttribute("fill", fill);
portNode.setAttribute("stroke", stroke);
portNode.setAttribute("stroke-width", String(strokeWidth));
}
}
};
function renderTitle(g, model) {
const title = model.get("title") || "";
const size = model.size();
const attrs = model.get("attrs") || {};
const rx = attrs["base-node-body"]?.rx ?? 4;
const ry = attrs["base-node-body"]?.ry ?? 4;
const style = model.get("style") || {};
const titleHeight = style.header?.height ?? 24;
const titleFontSize = style.header?.fontSize ?? 12;
const titleTop = style.header?.title?.y ?? 0;
const titleLeft = style.header?.title?.x ?? 0;
const textAlign = style.header?.textAlign ?? "left";
const titlePath = V_default("path", {
d: `
M0,${titleHeight}
L0,${ry}
Q0,0 ${rx},0
L${size.width - rx},0
Q${size.width},0 ${size.width},${ry}
L${size.width},${titleHeight}
Z
`,
fill: style.header?.background ?? "#373C44"
}).node;
const textAnchor = textAlign === "left" ? "start" : textAlign === "right" ? "end" : "middle";
let startX = 0;
switch (textAlign) {
case "left":
startX = 10;
break;
case "right":
startX = size.width - 10;
break;
case "center":
startX = size.width / 2;
break;
}
const text = V_default("text", {
x: startX + titleLeft,
y: titleHeight / 2 + titleFontSize / 2 - 3 + titleTop,
fill: "#fff",
fontSize: titleFontSize,
textAnchor
}).node;
text.textContent = title;
g.appendChild(titlePath);
g.appendChild(text);
}
//#endregion
//#region src/adapters/joint/ScrollerController.ts
var ScrollerController = class ScrollerController extends Listener {
_viewport = {
x: 0,
y: 0,
scale: 1
};
_isDragging = false;
_lastClientX = 0;
_lastClientY = 0;
_spacePressed = false;
static ZOOM_SENSITIVITY = .005;
get isDragging() {
return this._isDragging;
}
get viewport() {
return this._viewport;
}
get scale() {
return this._viewport.scale;
}
set scale(value) {
this._viewport.scale = value;
this.updatePaperTransform();
}
get position() {
return {
x: this._viewport.x,
y: this._viewport.y
};
}
set position(value) {
this._viewport.x = value.x;
this._viewport.y = value.y;
this.updatePaperTransform();
}
startListening() {
const [{ paper }] = this.callbackArguments;
paper.el.addEventListener("keydown", this.onKeyDown);
paper.el.addEventListener("keyup", this.onKeyUp);
paper.el.addEventListener("selectstart", this.onSelectStart);
this.initPaperEvents();
}
initPaperEvents() {
const [{ paper }] = this.callbackArguments;
const el = paper.el;
el.addEventListener("mousedown", this.onMouseDown);
el.addEventListener("mousemove", this.onMouseMove);
el.addEventListener("mouseup", this.onMouseUp);
paper.el.addEventListener("selectstart", this.onSelectStart);
el.addEventListener("wheel", this.onMouseWheel, { passive: true });
}
onKeyDown = (e) => {
const [{ paper }] = this.callbackArguments;
if (e.target !== paper.el) return;
if (e.code === "Space") {
this._spacePressed = true;
paper.el.style.cursor = "grab";
}
};
onKeyUp = (e) => {
const [{ paper }] = this.callbackArguments;
if (e.code === "Space") {
this._spacePressed = false;
paper.el.style.cursor = "";
}
};
onMouseDown = (e) => {
const [{ paper }] = this.callbackArguments;
const el = paper.el;
if (e.button === 0 && this._spacePressed || e.button === 2 && !this._spacePressed) {
e.stopPropagation();
e.preventDefault();
this._isDragging = true;
this._lastClientX = e.clientX;
this._lastClientY = e.clientY;
el.style.cursor = "grabbing";
}
};
onMouseMove = (e) => {
const [{ paper, graph }] = this.callbackArguments;
if (!this.isDragging) return;
const dx = e.clientX - this._lastClientX;
const dy = e.clientY - this._lastClientY;
this._viewport.x += dx;
this._viewport.y += dy;
paper.translate(this._viewport.x, this._viewport.y);
graph.emit("viewport:change", this._viewport);
this._lastClientX = e.clientX;
this._lastClientY = e.clientY;
};
onMouseUp = () => {
const [{ paper }] = this.callbackArguments;
if (this.isDragging) {
this._isDragging = false;
if (this._spacePressed) paper.el.style.cursor = "grab";
else paper.el.style.cursor = "";
}
};
onMouseWheel = (e) => {
const [{ paper, graph }] = this.callbackArguments;
if (this.isDragging) return;
const rect = paper.el.getBoundingClientRect();
const point = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
const localPoint = paper.clientToLocalPoint(point);
this.viewport.scale += e.deltaY * -ScrollerController.ZOOM_SENSITIVITY;
this.viewport.scale = Math.min(Math.max(.2, this.viewport.scale), 5);
paper.scale(this.viewport.scale, this.viewport.scale);
const newLocalPoint = paper.clientToLocalPoint(point);
this.viewport.x += (newLocalPoint.x - localPoint.x) * this.viewport.scale;
this.viewport.y += (newLocalPoint.y - localPoint.y) * this.viewport.scale;
paper.translate(this.viewport.x, this.viewport.y);
paper.setGridSize(10 / this.viewport.scale);
graph.emit("viewport:change", this._viewport);
};
updatePaperTransform() {
const [{ paper }] = this.callbackArguments;
paper.scale(this.viewport.scale, this.viewport.scale);
paper.translate(this.viewport.x, this.viewport.y);
paper.setGridSize(10 / this.viewport.scale);
}
onSelectStart = (e) => {
if (this.isDragging) e.preventDefault();
};
stopListening() {
super.stopListening();
const [{ paper }] = this.callbackArguments;
const el = paper.el;
el.removeEventListener("mousedown", this.onMouseDown);
el.removeEventListener("mousemove", this.onMouseMove);
el.removeEventListener("mouseup", this.onMouseUp);
paper.el.removeEventListener("selectstart", this.onSelectStart);
el.removeEventListener("wheel", this.onMouseWheel);
paper.el.removeEventListener("keydown", this.onKeyDown);
paper.el.removeEventListener("keyup", this.onKeyUp);
}
};
//#endregion
//#region src/adapters/joint/SelectionController.ts
var SelectionController = class SelectionController extends Listener {
rect;
startPoint;
pointerDownTime = 0;
hasMovedEnough = false;
static MIN_MOVE_DISTANCE = 3;
static MIN_PRESS_DURATION = 100;
startListening() {
const [{ paper }] = this.callbackArguments;
this.listenTo(paper, "blank:pointerdown", this.onPointerDown, this);
this.listenTo(paper, "blank:pointermove", this.onPointerMove, this);
this.listenTo(paper, "blank:pointerup", this.onPointerUp, this);
}
onPointerDown({ paper }, evt) {
const clientX = evt.clientX;
const clientY = evt.clientY;
const localPoint = paper.clientToLocalPoint(clientX, clientY);
this.startPoint = paper.localToPagePoint(localPoint.x, localPoint.y);
this.pointerDownTime = Date.now();
this.hasMovedEnough = false;
}
onPointerMove({ paper, className }, evt) {
if (!this.startPoint) return;
const clientX = evt.clientX;
const clientY = evt.clientY;
const localPoint = paper.clientToLocalPoint(clientX, clientY);
const currentPoint = paper.localToPagePoint(localPoint.x, localPoint.y);
const dx = Math.abs(currentPoint.x - this.startPoint.x);
const dy = Math.abs(currentPoint.y - this.startPoint.y);
const moved = dx > SelectionController.MIN_MOVE_DISTANCE || dy > SelectionController.MIN_MOVE_DISTANCE;
if (!this.hasMovedEnough && moved) {
const duration = Date.now() - (this.pointerDownTime ?? 0);
if (duration > SelectionController.MIN_PRESS_DURATION) {
this.rect = V_default("rect", {
x: this.startPoint.x,
y: this.startPoint.y,
width: 1,
height: 1,
class: className ?? "",
stroke: "#3498db",
"stroke-width": 1,
fill: "rgba(52,152,219,0.15)"
}).node;
paper.svg.appendChild(this.rect);
this.hasMovedEnough = true;
}
}
if (this.rect && this.hasMovedEnough) {
const x1 = Math.min(this.startPoint.x, currentPoint.x);
const y1 = Math.min(this.startPoint.y, currentPoint.y);
const x2 = Math.max(this.startPoint.x, currentPoint.x);
const y2 = Math.max(this.startPoint.y, currentPoint.y);
V_default(this.rect).attr({
x: x1,
y: y1,
width: x2 - x1,
height: y2 - y1
});
}
}
onPointerUp({ paper, onSelected }, evt) {
if (!this.rect || !this.startPoint) return;
const clientX = evt.clientX;
const clientY = evt.clientY;
const currentPoint = paper.clientToLocalPoint(clientX, clientY);
const startPoint = paper.pageToLocalPoint(this.startPoint.x, this.startPoint.y);
const x1 = Math.min(startPoint.x, currentPoint.x);
const y1 = Math.min(startPoint.y, currentPoint.y);
const x2 = Math.max(startPoint.x, currentPoint.x);
const y2 = Math.max(startPoint.y, currentPoint.y);
const bbox = new Rect(x1, y1, x2 - x1, y2 - y1);
const elements = paper.model.getElements();
const links = paper.model.getLinks();
const selected = elements.filter((el) => bbox.intersect(el.getBBox()));
const selectedLinks = links.filter((link) => {
const linkView = paper.findViewByModel(link);
if (!linkView) return false;
const path = linkView.findNode("line");
if (!path) return false;
return pathIntersectsRect(path, bbox);
});
onSelected([...selected, ...selectedLinks]);
this.rect.remove();
this.rect = void 0;
this.startPoint = void 0;
}
};
function pathIntersectsRect(path, rect) {
const length = path.getTotalLength();
const step = 5;
for (let i = 0; i <= length; i += step) {
const pt = path.getPointAtLength(i);
if (pt.x >= rect.x && pt.x <= rect.x + rect.width && pt.y >= rect.y && pt.y <= rect.y + rect.height) return true;
}
return false;
}
//#endregion
//#region src/adapters/joint/GroupEffectController.ts
var GroupEffectController = class extends Listener {
rect;
cells = [];
nodes = [];
dragStartPoint;
dragStartClientPoint;
cellsStartPositions = /* @__PURE__ */ new Map();
rectStartPos;
moveNodes = [];
startListening() {
const [{ paper }] = this.callbackArguments;
this.listenTo(paper, "element:pointerdown", this.onElementPointerDown, this);
this.listenTo(paper, "element:pointerup", this.onElementPointerUp, this);
this.listenTo(paper, "element:pointermove", this.onElementPointerMove, this);
this.listenTo(paper, "scale", this.onScale, this);
this.listenTo(paper, "translate", this.onTranslate, this);
}
onScale() {
if (this.cells.length <= 1) return;
this.clearGroupEffect();
this.drawGroupEffect();
}
onTranslate() {
if (this.cells.length <= 1) return;
this.clearGroupEffect();
this.drawGroupEffect();
}
onElementPointerDown({ paper, adapter }, elementView) {
if (!elementView.model.isElement()) return;
this.dragStartPoint = {
x: elementView.model.position().x,
y: elementView.model.position().y
};
this.moveNodes = [elementView.model];
this.rectStartPos = void 0;
if (this.nodes.includes(elementView.model)) {
this.moveNodes = this.nodes;
this.rectStartPos = {
x: this.rect ? parseFloat(this.rect.getAttribute("x") || "0") : 0,
y: this.rect ? parseFloat(this.rect.getAttribute("y") || "0") : 0
};
this.dragStartClientPoint = paper.localToClientPoint(this.dragStartPoint.x, this.dragStartPoint.y);
}
this.moveNodes.forEach((cell) => {
if (!cell.isElement()) return;
this.cellsStartPositions.set(cell.id, cell.position());
const node = adapter.cellsMap.get(cell.id)?.cell;
if (!node || !("position" in node)) return;
adapter.graph.emit("node:dragstart", node);
});
}
onElementPointerMove({ adapter, paper }, elementView) {
if (!this.moveNodes.length) return;
if (!this.dragStartPoint) return;
const pos = elementView.model.position();
const clientPos = paper.localToClientPoint(pos.x, pos.y);
const clientOffsetX = clientPos.x - (this.dragStartClientPoint?.x ?? 0);
const clientOffsetY = clientPos.y - (this.dragStartClientPoint?.y ?? 0);
const offsetX = pos.x - (this.dragStartPoint?.x ?? 0);
const offsetY = pos.y - (this.dragStartPoint?.y ?? 0);
this.moveNodes.forEach((cell) => {
const node = adapter.cellsMap.get(cell.id)?.cell;
if (cell.id !== elementView.model.id) {
const startPos = this.cellsStartPositions.get(cell.id);
cell.position(startPos.x + offsetX, startPos.y + offsetY);
}
if (node && "position" in node) {
const bbox = adapter.getElementBBox(elementView);
node._bbox = bbox;
adapter.graph.emit("node:dragmove", node);
}
});
if (this.rect && this.rectStartPos) {
this.rect.setAttribute("x", String(this.rectStartPos.x + clientOffsetX));
this.rect.setAttribute("y", String(this.rectStartPos.y + clientOffsetY));
}
}
onElementPointerUp({ adapter, paper }, elementView) {
if (!this.moveNodes.length) return;
const dragStart = this.dragStartPoint;
this.dragStartPoint = void 0;
if (!dragStart) return;
const dragEnd = elementView.model.position();
const offsetX = dragEnd.x - (dragStart?.x ?? 0);
const offsetY = dragEnd.y - (dragStart?.y ?? 0);
const clientPos = paper.localToClientPoint(dragEnd.x, dragEnd.y);
const clientOffsetX = clientPos.x - (this.dragStartClientPoint?.x ?? 0);
const clientOffsetY = clientPos.y - (this.dragStartClientPoint?.y ?? 0);
if (dragEnd.x !== dragStart.x || dragEnd.y !== dragStart.y) {
adapter.graph.startTransaction();
this.moveNodes.forEach((node) => {
const startPos = this.cellsStartPositions.get(node.id);
node.position(startPos.x + offsetX, startPos.y + offsetY);
const instance = adapter.cellsMap.get(node.id)?.cell;
if (instance && "position" in instance) {
instance.position = {
x: startPos.x + offsetX,
y: startPos.y + offsetY
};
const bbox = adapter.getElementBBox(elementView);
instance._bbox = bbox;
adapter.graph.emit("node:dragend", instance);
}
});
adapter.graph.commitTransaction();
if (this.rect && this.rectStartPos) {
this.rect.setAttribute("x", String(this.rectStartPos.x + clientOffsetX));
this.rect.setAttribute("y", String(this.rectStartPos.y + clientOffsetY));
}
this.rectStartPos = void 0;
this.moveNodes = [];
this.cellsStartPositions.clear();
}
}
groupCells(cells) {
this.cells = cells;
this.nodes = cells.filter((c) => c.isElement());
this.updateGroupEffect();
this.updateCellsHighlight();
}
ungroupCells(cells) {
this.cells = this.cells.filter((c) => !cells.includes(c));
this.nodes = this.cells.filter((c) => c.isElement());
this.updateGroupEffect();
this.updateCellsHighlight();
}
clearGroups() {
this.cells = [];
this.nodes = [];
this.updateGroupEffect();
this.updateCellsHighlight();
}
updateCellsHighlight() {
const [{ adapter }] = this.callbackArguments;
adapter.joint.clearHighlights();
adapter.joint.highlightCells(this.cells);
}
drawGroupEffect() {
const bbox = this.calculateBoundingBox();
const [{ paper }] = this.callbackArguments;
if (!this.rect) {
this.rect = V_default("rect", {
x: 0,
y: 0,
width: 100,
height: 100,
class: "selection-effect",
stroke: "#3c3ce7ff",
"stroke-width": 2,
fill: "rgba(60, 60, 60, 0.15)",
rx: 4,
ry: 4,
"pointer-events": "none"
}).node;
paper.svg.appendChild(this.rect);
}
this.rect.setAttribute("x", String(bbox.x - 10));
this.rect.setAttribute("y", String(bbox.y - 10));
this.rect.setAttribute("width", String(bbox.width + 20));
this.rect.setAttribute("height", String(bbox.height + 20));
}
updateGroupEffect() {
if (this.nodes.length > 1) this.drawGroupEffect();
else this.clearGroupEffect();
}
clearGroupEffect() {
if (this.rect) {
this.rect.remove();
this.rect = void 0;
}
}
calculateBoundingBox() {
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
const [{ paper }] = this.callbackArguments;
this.nodes.forEach((node) => {
const bbox = paper.localToPaperRect(node.getBBox());
minX = Math.min(minX, bbox.x);
minY = Math.min(minY, bbox.y);
maxX = Math.max(maxX, bbox.x + bbox.width);
maxY = Math.max(maxY, bbox.y + bbox.height);
});
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY
};
}
};
//#endregion
//#region src/adapters/joint/JointGraph.ts
config.classNamePrefix = "bpgraph-";
const custom = {
BlueprintNode,
BlueprintNodeView
};
const namespace = {
custom,
standard: standard_exports
};
var JointGraph = class {
graph;
paper;
scrollerController;
selectionController;
groupEffectController;
adapter;
constructor(adapter, options) {
this.adapter = adapter;
this.graph = new Graph({});
this.paper = new Paper({
model: this.graph,
cellViewNamespace: namespace,
frozen: false,
async: true,
clickThreshold: 5,
snapLinks: { radius: 20 },
linkPinning: false,
preventDefaultViewAction: false,
preventDefaultBlankAction: false,
sorting: Paper.sorting.APPROX,
interactive: function(cellView) {
return cellView.model.isElement();
},
defaultAnchor: (view, magnet, ...rest) => {
const group = view.findAttribute("port-group", magnet);
const anchorFn = group === "in" ? left : right;
return anchorFn(view, magnet, ...rest);
},
defaultConnectionPoint: { name: "anchor" },
defaultConnector: (sourcePoint, targetPoint, routePoints, opt, linkView) => {
const link = linkView.model;
const sourcePortId = link.get("source").port;
const isSourceIn = sourcePortId?.startsWith("in-");
const isSourceOut = sourcePortId?.startsWith("out-");
if (isSourceIn) {
opt.sourceDirection = curve.TangentDirections.LEFT;
opt.targetDirection = curve.TangentDirections.RIGHT;
} else if (isSourceOut) {
opt.sourceDirection = curve.TangentDirections.RIGHT;
opt.targetDirection = curve.TangentDirections.LEFT;
}
return curve(sourcePoint, targetPoint, routePoints, opt, linkView);
},
validateMagnet: () => {
this.adapter.graph.emit("start:connecting");
return true;
},
defaultLink: () => new Link({ attrs: { line: {
strokeWidth: this.adapter.graph.nodeRegistry.linkStyle.strokeWidth || 1,
stroke: this.adapter.graph.nodeRegistry.linkStyle.stroke || "rgba(255,255,255,0.5)",
targetMarker: { type: "none" }
} } }),
validateConnection: (sourceView, sourceMagnet, targetView, targetMagnet) => {
if (sourceView === targetView) return false;
const target = targetView.model;
if (target.isLink()) return false;
const sourceGroup = sourceView.findAttribute("port-group", sourceMagnet);
const targetGroup = targetView.findAttribute("port-group", targetMagnet);
if (sourceGroup === "in" && targetGroup !== "out") return false;
if (sourceGroup === "out" && targetGroup !== "in") return false;
if (sourceView && sourceMagnet && targetMagnet && targetView) {
const sourcePortId = sourceView.findAttribute("port", sourceMagnet) || "";
const targetPortId = targetView.findAttribute("port", targetMagnet) || "";
const sourcePort = sourcePortId.startsWith("out-") ? sourceView.model.get("outputs")?.find((port) => port.id === sourcePortId) : sourceView.model.get("inputs")?.find((port) => port.id === sourcePortId);
const targetPort = targetPortId.startsWith("in-") ? targetView.model.get("inputs")?.find((port) => port.id === targetPortId) : targetView.model.get("outputs")?.find((port) => port.id === targetPortId);
const sourceNode = this.adapter.findNode(sourceView.model.id);
const targetNode = this.adapter.findNode(targetView.model.id);
if (sourcePort && targetPort) return this.adapter.graph.validateConnection(sourceNode, sourcePort, targetNode, targetPort);
}
return true;
},
...options
});
this.initialize();
}
initialize() {
this.scrollerController = new ScrollerController({
paper: this.paper,
graph: this.adapter.graph
});
this.selectionController = new SelectionController({
paper: this.paper,
onSelected: (cells) => {
this.paper.trigger("selection:changed", cells);
}
});
this.groupEffectController = new GroupEffectController({
adapter: this.adapter,
paper: this.paper,
className: "selection-rectangle"
});
this.initializeControllers();
this.paper.on("link:connect", (linkView) => {
const source = linkView.model.get("source");
const target = linkView.model.get("target");
const sourceNodeView = this.paper.findViewByModel(source.id);
const targetNodeView = this.paper.findViewByModel(target.id);
const sourcePortId = source.port;
const targetPortId = target.port;
if (sourceNodeView && sourcePortId && targetNodeView && targetPortId) {
sourceNodeView.highlightPort(sourcePortId);
targetNodeView.highlightPort(targetPortId);
}
});
this.graph.on("remove", (cell) => {
if (cell.isLink()) {
const link = cell;
const source = link.get("source");
const target = link.get("target");
const sourceNodeView = this.paper.findViewByModel(source.id);
const targetNodeView = this.paper.findViewByModel(target.id);
const sourcePortId = source.port;
const targetPortId = target.port;
if (!sourcePortId || !targetPortId) return;
const sourceLinks = this.graph.getConnectedLinks(sourceNodeView.model, {
inbound: true,
outbound: true
});
const targetLinks = this.graph.getConnectedLinks(targetNodeView.model, {
inbound: true,
outbound: true
});
const stillConnectedFromSource = sourceLinks.some((l) => l.get("source").port === sourcePortId || l.get("target").port === sourcePortId);
const stillConnectedFromTarget = targetLinks.some((l) => l.get("source").port === targetPortId || l.get("target").port === targetPortId);
if (sourceNodeView && sourcePortId && !stillConnectedFromSource) sourceNodeView.unhighlightPort(sourcePortId);
if (targetNodeView && targetPortId && !stillConnectedFromTarget) targetNodeView.unhighlightPort(targetPortId);
}
});
}
initializeControllers() {
if (this.paper.el) {
this.scrollerController.stopListening();
this.selectionController.stopListening();
this.groupEffectController.stopListening();
this.scrollerController.startListening();
this.selectionController.startListening();
this.groupEffectController.startListening();
this.paper.el.removeEventListener("keydown", this.onKeyDown);
this.paper.el.removeEventListener("keyup", this.onKeyUp);
this.paper.el.addEventListener("keydown", this.onKeyDown);
this.paper.el.addEventListener("keyup", this.onKeyUp);
}
}
addNode(instance, style) {
const inputs = instance.inputs;
con