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 },
|
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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user