graflow: subflow reference nodes & exit subflow

This commit is contained in:
STEINNI
2026-02-25 18:25:33 +00:00
parent e5bd91cd9f
commit 0b4b7e2a2f
16 changed files with 5687 additions and 761 deletions
+28 -2
View File
@@ -48,6 +48,21 @@
.bzgf-node [data-id="out2"]{ top: 25%; } .bzgf-node [data-id="out2"]{ top: 25%; }
.bzgf-node [data-id="out3"]{ top: 75%; } .bzgf-node [data-id="out3"]{ top: 75%; }
.bzgf-node[data-nodetype="refnodein"], .bzgf-node[data-nodetype="refnodeout"] {
width:3em;
height:3em;
padding: 2px;
}
.bzgf-node[data-nodetype="refnodein"] .body, .bzgf-node[data-nodetype="refnodeout"] .body{
border-radius: 50%;
width: 3em;
height: 3em;
display: flex;
align-items: center;
justify-content: center;
}
.bzgf-node[data-nodetype="refnodein"] .body{ background: #0F0; }
.bzgf-node[data-nodetype="refnodeout"] .body{ background: #FF0; }
.bzgf-wire{ stroke: var(--eicui-base-color-info); stroke-width: 4px; stroke-dasharray: 10,5; } .bzgf-wire{ stroke: var(--eicui-base-color-info); stroke-width: 4px; stroke-dasharray: 10,5; }
</style> </style>
@@ -66,7 +81,18 @@
<div class="port" data-type="out" data-id="out2" data-direction="e"></div> <div class="port" data-type="out" data-id="out2" data-direction="e"></div>
<div class="port" data-type="out" data-id="out3" data-direction="e"></div> <div class="port" data-type="out" data-id="out3" data-direction="e"></div>
</div> </div>
</template> </template>
<template>
<div class="bzgf-node" data-nodetype="refnodein">
<div class="body">{parentport}</div>
<div class="port" data-type="out" data-id="out1" data-direction="e"></div>
</div>
</template>
<template>
<div class="bzgf-node" data-nodetype="refnodeout">
<div class="body">{parentport}</div>
<div class="port" data-type="in" data-id="in1" data-direction="w"></div>
</div>
</template>
+18
View File
@@ -96,6 +96,12 @@
.bzgf-node[data-nodetype="input"] .title, .bzgf-node[data-nodetype="input"] .title,
.bzgf-node[data-nodetype="console"] .title{ background: #555; } .bzgf-node[data-nodetype="console"] .title{ background: #555; }
.bzgf-node[data-nodetype="refnodein"] .body, .bzgf-node[data-nodetype="refnodeout"] body{
border-radius: 50%;
}
.bzgf-node[data-nodetype="refnodein"] .body{ background: #0F0; }
.bzgf-node[data-nodetype="refnodeout"] .body{ background: #FF0; }
.bzgf-wire{ stroke: #0AF; stroke-width: 2; } .bzgf-wire{ stroke: #0AF; stroke-width: 2; }
</style> </style>
@@ -171,3 +177,15 @@
</div> </div>
</div> </div>
</template> </template>
<template>
<div class="bzgf-node" data-nodetype="refnodein">
<div class="body">{parentport}</div>
</div>
</template>
<template>
<div class="bzgf-node" data-nodetype="refnodeout">
<div class="body">{parentport}</div>
</div>
</template>
+3
View File
@@ -55,6 +55,9 @@
<script> <script>
window.addEventListener('load',()=>{ window.addEventListener('load',()=>{
const grflw1 = document.querySelector('bz-graflow.compunet') const grflw1 = document.querySelector('bz-graflow.compunet')
grflw1.addEventListener('subflowLoaded',
(evt) => { evt.target.querySelector('.demooptions').style.display = 'none'; }
)
document.querySelector('[data-trigger="onAutoplace1H"]').addEventListener('click', document.querySelector('[data-trigger="onAutoplace1H"]').addEventListener('click',
(evt) => { grflw1.autoPlace('horizontal', 80, 30, 1000, document.querySelector('[data-id="compunet"]').value) } (evt) => { grflw1.autoPlace('horizontal', 80, 30, 1000, document.querySelector('[data-id="compunet"]').value) }
) )
+10 -2
View File
@@ -9,8 +9,16 @@
{ "nodeType": "inc", { "nodeType": "inc",
"subflow": "/app/assets/json/bzGraflow/testFlowEic.json", "subflow": "/app/assets/json/bzGraflow/testFlowEic.json",
"portLinks": [ "portLinks": [
{ "parentPort": ["in1"], "subflowPort": ["inp1"] }, { "refNodeType": "refnodein", "refnodePort": "out1",
{ "parentPort": ["out1"], "subflowPort": ["out1"] } "parentPort": "in1",
"subflowNode":"aze2", "subflowPort": "in1",
"direction": "in"
},
{ "refNodeType": "refnodeout", "refnodePort": "in1",
"parentPort": "out1",
"subflowNode":"aze5", "subflowPort": "out1",
"direction": "out"
}
], ],
"id": "aze2", "id": "aze2",
"coords": { "x": 220, "y": 10} "coords": { "x": 220, "y": 10}
Binary file not shown.
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
+618
View File
@@ -0,0 +1,618 @@
@font-face {
font-family: 'glyphs';
src: url('/app/assets/styles/fonts/glyphs.eot');
src: url('/app/assets/styles/fonts/glyphs.eot') format('embedded-opentype'),
url('/app/assets/styles/fonts/glyphs.ttf') format('truetype'),
url('/app/assets/styles/fonts/glyphs.woff') format('woff'),
url('/app/assets/styles/fonts/glyphs.svg') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
}
[class^="icon-"], [class*=" icon-"] {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'glyphs' !important;
speak: never;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
span[class*="icon-"][primary]:before { color: var(--app-color-primary); }
span[class*="icon-"][secondary] { color: var(--app-color-secondary); }
span[class*="icon-"][success] { color: var(--app-color-success); }
span[class*="icon-"][danger] { color: var(--app-color-danger); }
span[class*="icon-"][warning] { color: var(--app-color-warning); }
span[class*="icon-"][accent] { color: var(--app-color-accent); }
span[class*="icon-"][info] { color: var(--app-color-info); }
[class*="icon-"][xxsmall] { font-size: var(--eicui-base-icon-size-2xs); }
[class*="icon-"][xsmall] { font-size: var(--eicui-base-icon-size-xs) !important; }
[class*="icon-"][small] { font-size: var(--eicui-base-icon-size-s) !important; }
[class*="icon-"][medium] { font-size: var(--eicui-base-icon-size-m) !important; }
[class*="icon-"][large] { font-size: var(--eicui-base-icon-size-l) !important; }
[class*="icon-"][xlarge] { font-size: var(--eicui-base-icon-size-xl) !important; }
[class*="icon-"][xxlarge] { font-size: var(--eicui-base-icon-size-2xl); }
[class*="icon-"][xxxlarge] { font-size: var(--eicui-base-icon-size-3xl); }
[class*="icon-"][xxxxlarge] { font-size: var(--eicui-base-icon-size-4xl); }
@keyframes spin {
0% { transform: rotate(0deg) ; }
100% { transform: rotate(360deg) ; }
}
@keyframes uturn {
0% { transform: rotate(-180deg) ; }
100% { transform: rotate(0deg) ; }
}
@keyframes uturn-ccw {
0% { transform: rotate(180deg) ; }
100% { transform: rotate(0deg) ; }
}
.spin { animation: spin 1s infinite linear; }
.arrow-expand:before {
content: "\f106" !important;
animation: uturn-ccw 0.3s 1 linear;
}
.arrow-collapse:before {
content: "\f107" !important;
animation: uturn 0.3s 1 normal linear;
}
.icon-backward1:before {
content: "\e905";
}
.icon-backward-step:before {
content: "\e943";
}
.icon-fast-forward:before {
content: "\e944";
}
.icon-pause2:before {
content: "\e945";
}
.icon-play1:before {
content: "\e946";
}
.icon-target1:before {
content: "\e948";
}
.icon-usb:before {
content: "\e949";
}
.icon-video-camera1:before {
content: "\e94a";
}
.icon-price-tag:before {
content: "\e93c";
}
.icon-align-center:before {
content: "\e933";
}
.icon-align-left:before {
content: "\e934";
}
.icon-align-right:before {
content: "\e935";
}
.icon-format-color:before {
content: "\e93b";
}
.icon-format-bold:before {
content: "\e936";
}
.icon-font-size-down:before {
content: "\e937";
}
.icon-format-underline:before {
content: "\e939";
}
.icon-big-bullet:before {
content: "\e928";
}
.icon-code:before {
content: "\e92b";
}
.icon-check-rounded:before {
content: "\e91f";
}
.icon-dashboard:before {
content: "\e92a";
}
.icon-more:before {
content: "\e923";
}
.icon-locked:before {
content: "\e925";
}
.icon-unlocked:before {
content: "\e926";
}
.icon-menu:before {
content: "\e920";
}
.icon-reply:before {
content: "\e921";
}
.icon-back:before {
content: "\e921";
}
.icon-link-ext:before {
content: "\e922";
}
.icon-share:before {
content: "\e929";
}
.icon-workflow:before {
content: "\e929";
}
.icon-health:before {
content: "\e927";
}
.icon-copy:before {
content: "\e915";
}
.icon-filter:before {
content: "\e91c";
}
.icon-deny:before {
content: "\e906";
}
.icon-home:before {
content: "\e91d";
}
.icon-pause1:before {
content: "\e916";
}
.icon-market:before {
content: "\e914";
}
.icon-thumbs-down:before {
content: "\e90b";
}
.icon-thumbs-up:before {
content: "\e90c";
}
.icon-trash:before {
content: "\e91e";
}
.icon-bolt:before {
content: "\e901";
}
.icon-check:before {
content: "\e902";
}
.icon-cancel:before {
content: "\e907";
}
.icon-close:before {
content: "\e908";
}
.icon-download:before {
content: "\e903";
}
.icon-envelope:before {
content: "\e909";
}
.icon-company:before {
content: "\e90a";
}
.icon-servers:before {
content: "\e904";
}
.icon-cabinet:before {
content: "\e94b";
}
.icon-cabinet1:before {
content: "\e94c";
}
.icon-camera1:before {
content: "\e94e";
}
.icon-camera2:before {
content: "\e94f";
}
.icon-film1:before {
content: "\e950";
}
.icon-chronometer:before {
content: "\e951";
}
.icon-chart:before {
content: "\e952";
}
.icon-lab:before {
content: "\e953";
}
.icon-satellite:before {
content: "\e954";
}
.icon-spaceinvaders:before {
content: "\e955";
}
.icon-bomb:before {
content: "\e957";
}
.icon-tools:before {
content: "\e958";
}
.icon-bus:before {
content: "\e959";
}
.icon-stop2:before {
content: "\e95a";
}
.icon-atom:before {
content: "\e95b";
}
.icon-globe:before {
content: "\e95c";
}
.icon-globe1:before {
content: "\e95d";
}
.icon-grid:before {
content: "\e95e";
}
.icon-flag:before {
content: "\e95f";
}
.icon-lock:before {
content: "\e960";
}
.icon-unlocked1:before {
content: "\e961";
}
.icon-camera3:before {
content: "\e962";
}
.icon-calculator:before {
content: "\e963";
}
.icon-diamond:before {
content: "\e965";
}
.icon-atom1:before {
content: "\e966";
}
.icon-syringe:before {
content: "\e967";
}
.icon-health1:before {
content: "\e968";
}
.icon-pill:before {
content: "\e969";
}
.icon-lab1:before {
content: "\e96a";
}
.icon-graph:before {
content: "\e96b";
}
.icon-review:before {
content: "\e917";
}
.icon-correction:before {
content: "\e918";
}
.icon-mediation:before {
content: "\e919";
}
.icon-writing:before {
content: "\e91b";
}
.icon-snapshot:before {
content: "\e92d";
}
.icon-toc:before {
content: "\e92e";
}
.icon-folder:before {
content: "\e92f";
}
.icon-folder-open:before {
content: "\e930";
}
.icon-folder-add:before {
content: "\e931";
}
.icon-folder-remove:before {
content: "\e932";
}
.icon-qrcode:before {
content: "\e938";
}
.icon-chat:before {
content: "\e96c";
}
.icon-bug:before {
content: "\e999";
}
.icon-font-size-up:before {
content: "\ea61";
}
.icon-format-italic:before {
content: "\ea64";
}
.icon-stop:before {
content: "\e92c";
}
.icon-play:before {
content: "\e900";
}
.icon-history:before {
content: "\e94d";
}
.icon-spinner:before {
content: "\e981";
}
.icon-cog:before {
content: "\e994";
}
.icon-star-empty:before {
content: "\e9d7";
}
.icon-star-half:before {
content: "\e9d8";
}
.icon-star-full:before {
content: "\e9d9";
}
.icon-heart:before {
content: "\e9da";
}
.icon-pdf:before {
content: "\eadf";
}
.icon-pen:before {
content: "\e910";
}
.icon-coaching:before {
content: "\e91a";
}
.icon-cart:before {
content: "\e93a";
}
.icon-phone:before {
content: "\e942";
}
.icon-hour-glass:before {
content: "\e90f";
}
.icon-refresh:before {
content: "\e984";
}
.icon-complaint:before {
content: "\e9a8";
}
.icon-evaluation:before {
content: "\e9b8";
}
.icon-link:before {
content: "\e9cb";
}
.icon-loop:before {
content: "\ea2d";
}
.icon-image:before {
content: "\e90d";
}
.icon-new:before {
content: "\e924";
}
.icon-map:before {
content: "\e947";
}
.icon-pin:before {
content: "\e947";
}
.icon-user-check:before {
content: "\e975";
}
.icon-cogs:before {
content: "\e995";
}
.icon-bin:before {
content: "\e9ac";
}
.icon-attachment:before {
content: "\e9cd";
}
.icon-xls:before {
content: "\eae2";
}
.icon-paint-format:before {
content: "\e93d";
}
.icon-camera:before {
content: "\e93e";
}
.icon-film:before {
content: "\e93f";
}
.icon-video-camera:before {
content: "\e940";
}
.icon-stack:before {
content: "\e941";
}
.icon-display:before {
content: "\e956";
}
.icon-database:before {
content: "\e964";
}
.icon-target:before {
content: "\e9b3";
}
.icon-tree:before {
content: "\e9bc";
}
.icon-play2:before {
content: "\ea15";
}
.icon-pause:before {
content: "\ea16";
}
.icon-stop1:before {
content: "\ea17";
}
.icon-previous:before {
content: "\ea18";
}
.icon-next:before {
content: "\ea19";
}
.icon-backward:before {
content: "\ea1a";
}
.icon-forward2:before {
content: "\ea1b";
}
.icon-embed:before {
content: "\ea7f";
}
.icon-steam:before {
content: "\eaac";
}
.icon-dropbox:before {
content: "\eaae";
}
.icon-tux:before {
content: "\eabd";
}
.icon-codepen:before {
content: "\eae8";
}
.icon-sort-asc:before {
content: "\e911";
}
.icon-sort-desc:before {
content: "\e912";
}
.icon-unsorted:before {
content: "\e913";
}
.icon-help:before {
content: "\e90e";
}
.icon-plus:before {
content: "\f067";
}
.icon-search:before {
content: "\f002";
}
.icon-user:before {
content: "\f007";
}
.icon-logoff:before {
content: "\f011";
}
.icon-edit:before {
content: "\f040";
}
.icon-checked:before {
content: "\f046";
}
.icon-info:before {
content: "\f05a";
}
.icon-expand:before {
content: "\f065";
}
.icon-warning:before {
content: "\f071";
}
.icon-calendar:before {
content: "\f073";
}
.icon-comment:before {
content: "\f075";
}
.icon-twitter:before {
content: "\f081";
}
.icon-facebook:before {
content: "\f082";
}
.icon-square-o:before {
content: "\f096";
}
.icon-website:before {
content: "\f0ac";
}
.icon-users:before {
content: "\f0c0";
}
.icon-list-ul:before {
content: "\f0ca";
}
.icon-table:before {
content: "\f0ce";
}
.icon-exchange:before {
content: "\f0ec";
}
.icon-alert:before {
content: "\f0f3";
}
.icon-chevron-left:before {
content: "\f100";
}
.icon-chevron-right:before {
content: "\f101";
}
.icon-angle-left:before {
content: "\f104";
}
.icon-angle-right:before {
content: "\f105";
}
.icon-angle-up:before {
content: "\f106";
}
.icon-angle-down:before {
content: "\f107";
}
.icon-zip:before {
content: "\f1c6";
}
.icon-send:before {
content: "\f1d8";
}
.icon-preview:before {
content: "\f1e5";
}
.icon-stats:before {
content: "\f200";
}
.icon-toggle-off:before {
content: "\f204";
}
.icon-toggle-on:before {
content: "\f205";
}
.icon-user-add:before {
content: "\f234";
}
.icon-calendar-plus:before {
content: "\f271";
}
.icon-calendar-minus:before {
content: "\f272";
}
.icon-calendar-failed:before {
content: "\f273";
}
.icon-calendar-check:before {
content: "\f274";
}
+2660 -509
View File
File diff suppressed because it is too large Load Diff
+138 -27
View File
@@ -18,6 +18,11 @@ class BZgraflow extends Buildoz{
e: { x: 1, y: 0 }, e: { x: 1, y: 0 },
w: { x: -1, y: 0 }, w: { x: -1, y: 0 },
} }
btnIcons = {
zoomin:'M992.262 88.604l-242.552 206.294c-25.074 22.566-51.89 32.926-73.552 31.926 57.256 67.068 91.842 154.078 91.842 249.176 0 212.078-171.922 384-384 384-212.076 0-384-171.922-384-384s171.922-384 384-384c95.098 0 182.108 34.586 249.176 91.844-1-21.662 9.36-48.478 31.926-73.552l206.294-242.552c35.322-39.246 93.022-42.554 128.22-7.356s31.892 92.898-7.354 128.22zM384 320c-141.384 0-256 114.616-256 256s114.616 256 256 256 256-114.616 256-256-114.614-256-256-256zM448 768h-128v-128h-128v-128h128v-128h128v128h128v128h-128z',
zoomout:'M992.262 88.604l-242.552 206.294c-25.074 22.566-51.89 32.926-73.552 31.926 57.256 67.068 91.842 154.078 91.842 249.176 0 212.078-171.922 384-384 384-212.076 0-384-171.922-384-384s171.922-384 384-384c95.098 0 182.108 34.586 249.176 91.844-1-21.662 9.36-48.478 31.926-73.552l206.294-242.552c35.322-39.246 93.022-42.554 128.22-7.356s31.892 92.898-7.354 128.22zM384 320c-141.384 0-256 114.616-256 256s114.616 256 256 256 256-114.616 256-256-114.614-256-256-256zM320 380v384h128V448z',
}
static _loadedNodeStyles = new Set() // Allow multi instances or re-loadNodes, but avoid reinjecting same styles ! static _loadedNodeStyles = new Set() // Allow multi instances or re-loadNodes, but avoid reinjecting same styles !
constructor(){ constructor(){
@@ -28,6 +33,10 @@ class BZgraflow extends Buildoz{
this.arrowDefs = null this.arrowDefs = null
} }
addIcon(el, name) {
el.innerHTML = `<svg viewBox="0 0 1024 1024" width="20" height="20" fill="#000000"><g transform="rotate(90 512 512)"><path d="${this.btnIcons[name]}"/></g></svg>`
}
connectedCallback() { connectedCallback() {
super.connectedCallback() super.connectedCallback()
const flowUrl = this.getBZAttribute('flow') const flowUrl = this.getBZAttribute('flow')
@@ -45,7 +54,7 @@ class BZgraflow extends Buildoz{
const style = document.createElement('style') const style = document.createElement('style')
//TODO kick this wart somewhere under a carpet //TODO kick this wart somewhere under a carpet
style.textContent = ` style.textContent = `
@import '/app/assets/styles/icons.css'; /*@import '/app/assets/styles/icons.css';*/
.bzgf-wires-container, .bzgf-wires-container,
.bzgf-nodes-container{ position: absolute; inset: 0; width: 100%; height: 100%; } .bzgf-nodes-container{ position: absolute; inset: 0; width: 100%; height: 100%; }
.bzgf-nodes-container .bzgf-node{ position:absolute; } .bzgf-nodes-container .bzgf-node{ position:absolute; }
@@ -57,13 +66,20 @@ class BZgraflow extends Buildoz{
border-style: none; border-style: none;
} }
.bzgf-nodes-container button.bzgf-zoom-in{ .bzgf-nodes-container button.bzgf-zoom-in{
width: 2em;
height: 2em;
z-index: 999; z-index: 999;
position: absolute; position: absolute;
top: -1em; top: -0.5em;
right: -1em; right: -1em;
color: #A00; color: black;
width: 2em;
height: 2em;
padding: 0;
}
bz-graflow button.bzgf-zoom-out{
z-index: 999;
position: absolute;
left: 5px;
top: 5px;
} }
` `
this.mainContainer.appendChild(style) this.mainContainer.appendChild(style)
@@ -102,6 +118,11 @@ class BZgraflow extends Buildoz{
await this.loadNodes(flowObj.nodesFile) await this.loadNodes(flowObj.nodesFile)
this.flow = flowObj.flow this.flow = flowObj.flow
this.refresh() this.refresh()
this.dispatchEvent(new CustomEvent('flowLoaded', {
detail: { url },
bubbles: true,
composed: true,
}))
} }
async loadNodes(url) { async loadNodes(url) {
@@ -135,6 +156,11 @@ class BZgraflow extends Buildoz{
// In isolated (shadow DOM) mode, styles must be injected per instance. // In isolated (shadow DOM) mode, styles must be injected per instance.
if(!isIsolated) BZgraflow._loadedNodeStyles.add(url) if(!isIsolated) BZgraflow._loadedNodeStyles.add(url)
} }
this.dispatchEvent(new CustomEvent('nodesLoaded', {
detail: { url },
bubbles: true,
composed: true,
}))
} }
addNode(node){ addNode(node){
@@ -153,17 +179,21 @@ class BZgraflow extends Buildoz{
this.stagedNodes[id].ports = Object.fromEntries(Array.from(portEls).map(item => ([item.dataset.id, { ...item.dataset, el:item }]))) this.stagedNodes[id].ports = Object.fromEntries(Array.from(portEls).map(item => ([item.dataset.id, { ...item.dataset, el:item }])))
if(node.subflow) { if(node.subflow) {
const btnEnterSubflow = document.createElement('button') const btnEnterSubflow = document.createElement('button')
btnEnterSubflow.classList.add('bzgf-zoom-in', 'icon-copy') btnEnterSubflow.classList.add('bzgf-zoom-in')
this.addIcon(btnEnterSubflow, 'zoomin')
btnEnterSubflow.addEventListener('click', () => { btnEnterSubflow.addEventListener('click', () => {
this.EnterSubflow(id) this.enterSubflow(id)
}) })
this.stagedNodes[id].appendChild(btnEnterSubflow) this.stagedNodes[id].appendChild(btnEnterSubflow)
} }
this.nodesContainer.append(this.stagedNodes[id]) this.nodesContainer.append(this.stagedNodes[id])
if(!this.flow.nodes.find(n => n.id === id)) {
this.flow.nodes.push(node)
}
return(this.stagedNodes[id]) return(this.stagedNodes[id])
} }
EnterSubflow(id){ enterSubflow(id){
const nodeEl = this.stagedNodes[id] const nodeEl = this.stagedNodes[id]
if(!nodeEl) return if(!nodeEl) return
@@ -178,14 +208,45 @@ class BZgraflow extends Buildoz{
const childEl = document.createElement('bz-graflow') const childEl = document.createElement('bz-graflow')
childEl.setAttribute('flow', flowUrl) childEl.setAttribute('flow', flowUrl)
childEl.setAttribute('tension', this.getBZAttribute('tension') || '60') childEl.setAttribute('tension', this.getBZAttribute('tension') || '60')
// Remember which node we "came from" so exitSubflow() can animate back to it.
childEl.dataset.enterNodeId = id
// Match the clicked node's border so the transition feels like we're "expanding" it. // Match the clicked node's border so the transition feels like we're "expanding" it.
const nodeStyle = getComputedStyle(nodeEl) const nodeStyle = getComputedStyle(nodeEl)
childEl.style.border = nodeStyle.border childEl.style.border = nodeStyle.border
childEl.style.borderRadius = nodeStyle.borderRadius childEl.style.borderRadius = nodeStyle.borderRadius
childEl.style.backgroundColor = nodeStyle.backgroundColor childEl.style.backgroundColor = nodeStyle.backgroundColor
const btnExitSubflow = document.createElement('button')
btnExitSubflow.classList.add('bzgf-zoom-out')
this.addIcon(btnExitSubflow, 'zoomout')
btnExitSubflow.addEventListener('click', () => {
this.exitSubflow(childEl)
})
childEl.appendChild(btnExitSubflow)
// Put the child in the exact same viewport rect as the parent (fixed overlay) // Put the child in the exact same viewport rect as the parent (fixed overlay)
this.Invade(this, childEl) this.invade(this, childEl)
childEl.addEventListener('flowLoaded', (e) => {
for(const portLink of flowNode.portLinks){
const nid = crypto.randomUUID()
childEl.addNode({
"nodeType": portLink.refNodeType,
"id": nid,
"markup": { "parentport": portLink.parentPort }
})
if(portLink.direction=='in') {
childEl.addWire({
"from": [nid, portLink.refnodePort],
"to": [portLink.subflowNode, portLink.subflowPort]
})
} else if(portLink.direction=='out') {
childEl.addWire({
"from": [portLink.subflowNode, portLink.subflowPort],
"to": [nid, portLink.refnodePort]
})
}
}
childEl.autoPlace('horizontal', 60, 60)
})
// Fade out the current (host) graflow while the child scales up // Fade out the current (host) graflow while the child scales up
this.hostContainer.style.opacity = '1' this.hostContainer.style.opacity = '1'
@@ -220,10 +281,15 @@ class BZgraflow extends Buildoz{
childEl.addEventListener('transitionend', (e) => { childEl.addEventListener('transitionend', (e) => {
if(e.propertyName !== 'transform') return if(e.propertyName !== 'transform') return
this.hostContainer.style.visibility = 'hidden' this.hostContainer.style.visibility = 'hidden'
this.dispatchEvent(new CustomEvent('subflowLoaded', {
detail: { flowUrl },
bubbles: true,
composed: true,
}))
}, { once:true }) }, { once:true })
} }
Invade(oldEl, newEl){ invade(oldEl, newEl){
const r = oldEl.getBoundingClientRect() const r = oldEl.getBoundingClientRect()
newEl.style.position = 'fixed' newEl.style.position = 'fixed'
newEl.style.left = r.left + 'px' newEl.style.left = r.left + 'px'
@@ -234,14 +300,58 @@ class BZgraflow extends Buildoz{
oldEl.appendChild(newEl) oldEl.appendChild(newEl)
} }
Evade(oldEl, newEl){ exitSubflow(childEl){
oldEl.style.visibility = 'visible' if(!childEl) return
oldEl.style.position = 'absolute'
oldEl.style.left = '0' const enterNodeId = childEl.dataset?.enterNodeId
oldEl.style.top = '0' const nodeEl = enterNodeId ? this.stagedNodes?.[enterNodeId] : null
oldEl.style.width = '0' if(!nodeEl){
oldEl.style.height = '0' // Fallback: no context => just restore parent & remove child
newEl.parentNode.removeChild(newEl) this.hostContainer.style.opacity = '1'
this.hostContainer.style.visibility = 'visible'
if(childEl.parentNode === this) this.removeChild(childEl)
return
}
// Compute target transform from full-size back to node rect (inverse of EnterSubflow)
const nodeBB = nodeEl.getBoundingClientRect()
const parentBB = this.getBoundingClientRect()
const sx0 = nodeBB.width / parentBB.width
const sy0 = nodeBB.height / parentBB.height
const tx0 = nodeBB.left - parentBB.left
const ty0 = nodeBB.top - parentBB.top
// Try to match duration to the child's transform transition (default 1000ms)
const transitionStr = childEl.style.transition || ''
const msMatch = transitionStr.match(/(\d+(?:\.\d+)?)ms/)
const durMs = msMatch ? parseFloat(msMatch[1]) : 1000
// Ensure parent is visible but faded-in during the shrink animation
this.hostContainer.style.visibility = 'visible'
this.hostContainer.style.opacity = '0'
this.hostContainer.style.transition = `opacity ${durMs}ms ease-in-out`
// Ensure child animates (it should already have the transform transition set)
childEl.style.transition = `transform ${durMs}ms ease-in-out`
childEl.getBoundingClientRect() // flush
requestAnimationFrame(() => {
// Shrink/move the child back into the original node
childEl.style.setProperty('--tx', tx0 + 'px')
childEl.style.setProperty('--ty', ty0 + 'px')
childEl.style.setProperty('--sx', sx0)
childEl.style.setProperty('--sy', sy0)
// Fade the parent back in
this.hostContainer.style.opacity = '1'
})
childEl.addEventListener('transitionend', (e) => {
if(e.propertyName !== 'transform') return
if(childEl.parentNode === this) this.removeChild(childEl)
// Cleanup: ensure parent is fully visible and no longer hidden
this.hostContainer.style.opacity = '1'
this.hostContainer.style.visibility = 'visible'
}, { once:true })
} }
addFakeNode(nid, x, y, w, h){ addFakeNode(nid, x, y, w, h){
@@ -269,10 +379,12 @@ class BZgraflow extends Buildoz{
this.stagedWires[id].classList.add('bzgf-wire') this.stagedWires[id].classList.add('bzgf-wire')
this.stagedWires[id].dataset.id = id this.stagedWires[id].dataset.id = id
this.wiresContainer.append(this.stagedWires[id]) this.wiresContainer.append(this.stagedWires[id])
if(!this.flow.links.find(l => l.from[0] === idNode1 && l.from[1] === idPort1 && l.to[0] === idNode2 && l.to[1] === idPort2)) {
this.flow.links.push(link)
}
return(this.stagedWires[id]) return(this.stagedWires[id])
} }
clear(){ clear(){
this.nodesContainer.innerHTML = '' this.nodesContainer.innerHTML = ''
this.wiresContainer.innerHTML = '' this.wiresContainer.innerHTML = ''
@@ -671,14 +783,13 @@ class BZgraflow extends Buildoz{
} }
} }
getLink(nid1, nid2){ getLink(nid1, nid2){
const real = this.flow.links.find(item => ((item.from[0]==nid1) && (item.to[0]==nid2))) let lnk = null
if(real) return(real) lnk = this.flow.links.find(item => ((item.from[0]==nid1) && (item.to[0]==nid2)))
const v = this._virtualLinks?.get(`${nid1}__${nid2}`) if(!lnk) {
if(v) return(v) lnk = this._virtualLinks?.get(`${nid1}__${nid2}`)
return(null) }
return(lnk)
} }
buildGraphStructures(nodes, links, includeLinkIndexes = false) { buildGraphStructures(nodes, links, includeLinkIndexes = false) {