graflow: subflow reference nodes & exit subflow
This commit is contained in:
165
bzGraflow.js
165
bzGraflow.js
@@ -18,6 +18,11 @@ class BZgraflow extends Buildoz{
|
||||
e: { 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 !
|
||||
|
||||
constructor(){
|
||||
@@ -28,6 +33,10 @@ class BZgraflow extends Buildoz{
|
||||
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() {
|
||||
super.connectedCallback()
|
||||
const flowUrl = this.getBZAttribute('flow')
|
||||
@@ -45,7 +54,7 @@ class BZgraflow extends Buildoz{
|
||||
const style = document.createElement('style')
|
||||
//TODO kick this wart somewhere under a carpet
|
||||
style.textContent = `
|
||||
@import '/app/assets/styles/icons.css';
|
||||
/*@import '/app/assets/styles/icons.css';*/
|
||||
.bzgf-wires-container,
|
||||
.bzgf-nodes-container{ position: absolute; inset: 0; width: 100%; height: 100%; }
|
||||
.bzgf-nodes-container .bzgf-node{ position:absolute; }
|
||||
@@ -57,13 +66,20 @@ class BZgraflow extends Buildoz{
|
||||
border-style: none;
|
||||
}
|
||||
.bzgf-nodes-container button.bzgf-zoom-in{
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
z-index: 999;
|
||||
position: absolute;
|
||||
top: -1em;
|
||||
top: -0.5em;
|
||||
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)
|
||||
@@ -102,6 +118,11 @@ class BZgraflow extends Buildoz{
|
||||
await this.loadNodes(flowObj.nodesFile)
|
||||
this.flow = flowObj.flow
|
||||
this.refresh()
|
||||
this.dispatchEvent(new CustomEvent('flowLoaded', {
|
||||
detail: { url },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}))
|
||||
}
|
||||
|
||||
async loadNodes(url) {
|
||||
@@ -135,6 +156,11 @@ class BZgraflow extends Buildoz{
|
||||
// In isolated (shadow DOM) mode, styles must be injected per instance.
|
||||
if(!isIsolated) BZgraflow._loadedNodeStyles.add(url)
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent('nodesLoaded', {
|
||||
detail: { url },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}))
|
||||
}
|
||||
|
||||
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 }])))
|
||||
if(node.subflow) {
|
||||
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', () => {
|
||||
this.EnterSubflow(id)
|
||||
this.enterSubflow(id)
|
||||
})
|
||||
this.stagedNodes[id].appendChild(btnEnterSubflow)
|
||||
}
|
||||
this.nodesContainer.append(this.stagedNodes[id])
|
||||
if(!this.flow.nodes.find(n => n.id === id)) {
|
||||
this.flow.nodes.push(node)
|
||||
}
|
||||
return(this.stagedNodes[id])
|
||||
}
|
||||
|
||||
EnterSubflow(id){
|
||||
enterSubflow(id){
|
||||
const nodeEl = this.stagedNodes[id]
|
||||
if(!nodeEl) return
|
||||
|
||||
@@ -178,14 +208,45 @@ class BZgraflow extends Buildoz{
|
||||
const childEl = document.createElement('bz-graflow')
|
||||
childEl.setAttribute('flow', flowUrl)
|
||||
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.
|
||||
const nodeStyle = getComputedStyle(nodeEl)
|
||||
childEl.style.border = nodeStyle.border
|
||||
childEl.style.borderRadius = nodeStyle.borderRadius
|
||||
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)
|
||||
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
|
||||
this.hostContainer.style.opacity = '1'
|
||||
@@ -220,10 +281,15 @@ class BZgraflow extends Buildoz{
|
||||
childEl.addEventListener('transitionend', (e) => {
|
||||
if(e.propertyName !== 'transform') return
|
||||
this.hostContainer.style.visibility = 'hidden'
|
||||
this.dispatchEvent(new CustomEvent('subflowLoaded', {
|
||||
detail: { flowUrl },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}))
|
||||
}, { once:true })
|
||||
}
|
||||
|
||||
Invade(oldEl, newEl){
|
||||
invade(oldEl, newEl){
|
||||
const r = oldEl.getBoundingClientRect()
|
||||
newEl.style.position = 'fixed'
|
||||
newEl.style.left = r.left + 'px'
|
||||
@@ -234,14 +300,58 @@ class BZgraflow extends Buildoz{
|
||||
oldEl.appendChild(newEl)
|
||||
}
|
||||
|
||||
Evade(oldEl, newEl){
|
||||
oldEl.style.visibility = 'visible'
|
||||
oldEl.style.position = 'absolute'
|
||||
oldEl.style.left = '0'
|
||||
oldEl.style.top = '0'
|
||||
oldEl.style.width = '0'
|
||||
oldEl.style.height = '0'
|
||||
newEl.parentNode.removeChild(newEl)
|
||||
exitSubflow(childEl){
|
||||
if(!childEl) return
|
||||
|
||||
const enterNodeId = childEl.dataset?.enterNodeId
|
||||
const nodeEl = enterNodeId ? this.stagedNodes?.[enterNodeId] : null
|
||||
if(!nodeEl){
|
||||
// Fallback: no context => just restore parent & remove child
|
||||
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){
|
||||
@@ -269,10 +379,12 @@ class BZgraflow extends Buildoz{
|
||||
this.stagedWires[id].classList.add('bzgf-wire')
|
||||
this.stagedWires[id].dataset.id = 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])
|
||||
}
|
||||
|
||||
|
||||
clear(){
|
||||
this.nodesContainer.innerHTML = ''
|
||||
this.wiresContainer.innerHTML = ''
|
||||
@@ -670,15 +782,14 @@ class BZgraflow extends Buildoz{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
getLink(nid1, nid2){
|
||||
const real = this.flow.links.find(item => ((item.from[0]==nid1) && (item.to[0]==nid2)))
|
||||
if(real) return(real)
|
||||
const v = this._virtualLinks?.get(`${nid1}__${nid2}`)
|
||||
if(v) return(v)
|
||||
return(null)
|
||||
let lnk = null
|
||||
lnk = this.flow.links.find(item => ((item.from[0]==nid1) && (item.to[0]==nid2)))
|
||||
if(!lnk) {
|
||||
lnk = this._virtualLinks?.get(`${nid1}__${nid2}`)
|
||||
}
|
||||
return(lnk)
|
||||
}
|
||||
|
||||
buildGraphStructures(nodes, links, includeLinkIndexes = false) {
|
||||
|
||||
Reference in New Issue
Block a user