graflow: entering subflow ok
This commit is contained in:
140
bzGraflow.js
140
bzGraflow.js
@@ -65,13 +65,6 @@ class BZgraflow extends Buildoz{
|
|||||||
right: -1em;
|
right: -1em;
|
||||||
color: #A00;
|
color: #A00;
|
||||||
}
|
}
|
||||||
.bzgf-nodes-container .bzgf-node.scaler {
|
|
||||||
transform-origin: top left;
|
|
||||||
transform:
|
|
||||||
translate(var(--tx, 0), var(--ty, 0))
|
|
||||||
scale(var(--sx, 1), var(--sy, 1));
|
|
||||||
transition: transform 300ms ease-in-out;
|
|
||||||
}
|
|
||||||
`
|
`
|
||||||
this.mainContainer.appendChild(style)
|
this.mainContainer.appendChild(style)
|
||||||
this.nodesContainer = document.createElement('div')
|
this.nodesContainer = document.createElement('div')
|
||||||
@@ -159,18 +152,18 @@ class BZgraflow extends Buildoz{
|
|||||||
this.stagedNodes[id].dataset.id = id
|
this.stagedNodes[id].dataset.id = id
|
||||||
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 btnZoomIn = document.createElement('button')
|
const btnEnterSubflow = document.createElement('button')
|
||||||
btnZoomIn.classList.add('bzgf-zoom-in', 'icon-copy')
|
btnEnterSubflow.classList.add('bzgf-zoom-in', 'icon-copy')
|
||||||
btnZoomIn.addEventListener('click', () => {
|
btnEnterSubflow.addEventListener('click', () => {
|
||||||
this.zoomIn(id)
|
this.EnterSubflow(id)
|
||||||
})
|
})
|
||||||
this.stagedNodes[id].appendChild(btnZoomIn)
|
this.stagedNodes[id].appendChild(btnEnterSubflow)
|
||||||
}
|
}
|
||||||
this.nodesContainer.append(this.stagedNodes[id])
|
this.nodesContainer.append(this.stagedNodes[id])
|
||||||
return(this.stagedNodes[id])
|
return(this.stagedNodes[id])
|
||||||
}
|
}
|
||||||
|
|
||||||
zoomIn(id){
|
EnterSubflow(id){
|
||||||
const nodeEl = this.stagedNodes[id]
|
const nodeEl = this.stagedNodes[id]
|
||||||
if(!nodeEl) return
|
if(!nodeEl) return
|
||||||
|
|
||||||
@@ -185,10 +178,17 @@ 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')
|
||||||
childEl.style.zIndex = '9999'
|
// 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
|
||||||
|
|
||||||
// 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, { hideOld:false })
|
this.Invade(this, childEl)
|
||||||
|
|
||||||
|
// Fade out the current (host) graflow while the child scales up
|
||||||
|
this.hostContainer.style.opacity = '1'
|
||||||
|
this.hostContainer.style.transition = 'opacity 1000ms ease-in-out'
|
||||||
|
|
||||||
// Initial transform so the full-size child "fits" inside the node
|
// Initial transform so the full-size child "fits" inside the node
|
||||||
const sx0 = nodeBB.width / parentBB.width
|
const sx0 = nodeBB.width / parentBB.width
|
||||||
@@ -199,7 +199,7 @@ class BZgraflow extends Buildoz{
|
|||||||
// Inline "scaler" (shadow styles don't apply to the child element)
|
// Inline "scaler" (shadow styles don't apply to the child element)
|
||||||
childEl.style.transformOrigin = 'top left'
|
childEl.style.transformOrigin = 'top left'
|
||||||
childEl.style.willChange = 'transform'
|
childEl.style.willChange = 'transform'
|
||||||
childEl.style.transition = 'transform 300ms ease-in-out'
|
childEl.style.transition = 'transform 1000ms ease-in-out'
|
||||||
childEl.style.transform = 'translate(var(--tx, 0px), var(--ty, 0px)) scale(var(--sx, 1), var(--sy, 1))'
|
childEl.style.transform = 'translate(var(--tx, 0px), var(--ty, 0px)) scale(var(--sx, 1), var(--sy, 1))'
|
||||||
childEl.style.setProperty('--tx', tx0 + 'px')
|
childEl.style.setProperty('--tx', tx0 + 'px')
|
||||||
childEl.style.setProperty('--ty', ty0 + 'px')
|
childEl.style.setProperty('--ty', ty0 + 'px')
|
||||||
@@ -213,24 +213,24 @@ class BZgraflow extends Buildoz{
|
|||||||
childEl.style.setProperty('--ty', '0px')
|
childEl.style.setProperty('--ty', '0px')
|
||||||
childEl.style.setProperty('--sx', 1)
|
childEl.style.setProperty('--sx', 1)
|
||||||
childEl.style.setProperty('--sy', 1)
|
childEl.style.setProperty('--sy', 1)
|
||||||
|
this.hostContainer.style.opacity = '0'
|
||||||
})
|
})
|
||||||
|
|
||||||
childEl.addEventListener('transitionend', (e) => {
|
childEl.addEventListener('transitionend', (e) => {
|
||||||
if(e.propertyName !== 'transform') return
|
if(e.propertyName !== 'transform') return
|
||||||
this.style.visibility = 'hidden'
|
this.hostContainer.style.visibility = 'hidden'
|
||||||
}, { once:true })
|
}, { once:true })
|
||||||
}
|
}
|
||||||
|
|
||||||
Invade(oldEl, newEl, { hideOld=true } = {}){
|
Invade(oldEl, newEl){
|
||||||
const r = oldEl.getBoundingClientRect()
|
const r = oldEl.getBoundingClientRect()
|
||||||
if(hideOld) oldEl.style.visibility = 'hidden'
|
|
||||||
newEl.style.position = 'fixed'
|
newEl.style.position = 'fixed'
|
||||||
newEl.style.left = r.left + 'px'
|
newEl.style.left = r.left + 'px'
|
||||||
newEl.style.top = r.top + 'px'
|
newEl.style.top = r.top + 'px'
|
||||||
newEl.style.width = r.width + 'px'
|
newEl.style.width = r.width + 'px'
|
||||||
newEl.style.height = r.height + 'px'
|
newEl.style.height = r.height + 'px'
|
||||||
newEl.style.display = 'block'
|
newEl.style.display = 'block'
|
||||||
oldEl.parentNode.appendChild(newEl)
|
oldEl.appendChild(newEl)
|
||||||
}
|
}
|
||||||
|
|
||||||
Evade(oldEl, newEl){
|
Evade(oldEl, newEl){
|
||||||
@@ -291,15 +291,39 @@ class BZgraflow extends Buildoz{
|
|||||||
|
|
||||||
if(forceAutoplace){
|
if(forceAutoplace){
|
||||||
const bb=this.getBoundingClientRect()
|
const bb=this.getBoundingClientRect()
|
||||||
//TODO compute tensions from ports
|
//TODO compute tensions from ports, height and width
|
||||||
if(bb.width > bb.height) this.autoPlace('horizontal', 80, 30, 500)
|
if(bb.width > bb.height) this.autoPlace('horizontal', 60, 60)
|
||||||
else this.autoPlace('vertical', 80, 30, 200)
|
else this.autoPlace('vertical', 60, 60)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert viewport (client) coordinates to this instance's SVG local coordinates.
|
||||||
|
// Required when the whole graflow is CSS-transformed (scale/translate), otherwise wire paths
|
||||||
|
// will be computed in the wrong coordinate space.
|
||||||
|
clientToSvg(x, y){
|
||||||
|
const svg = this.wiresContainer
|
||||||
|
const ctm = svg?.getScreenCTM?.()
|
||||||
|
if(ctm && ctm.inverse){
|
||||||
|
const inv = ctm.inverse()
|
||||||
|
if(svg?.createSVGPoint){
|
||||||
|
const pt = svg.createSVGPoint()
|
||||||
|
pt.x = x
|
||||||
|
pt.y = y
|
||||||
|
const p = pt.matrixTransform(inv)
|
||||||
|
return({ x: p.x, y: p.y })
|
||||||
|
}
|
||||||
|
if(typeof DOMPoint !== 'undefined'){
|
||||||
|
const p = new DOMPoint(x, y).matrixTransform(inv)
|
||||||
|
return({ x: p.x, y: p.y })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback: approximate using boundingClientRect (works only at scale=1)
|
||||||
|
const r = svg.getBoundingClientRect()
|
||||||
|
return({ x: x - r.left, y: y - r.top })
|
||||||
|
}
|
||||||
|
|
||||||
bezierNodes(idNode1, idPort1, idNode2, idPort2, tension=60) {
|
bezierNodes(idNode1, idPort1, idNode2, idPort2, tension=60) {
|
||||||
tension = parseInt(tension)
|
tension = parseInt(tension)
|
||||||
const svgRect = this.wiresContainer.getBoundingClientRect()
|
|
||||||
const node1 = this.stagedNodes[idNode1]
|
const node1 = this.stagedNodes[idNode1]
|
||||||
const port1 = node1.ports[idPort1]
|
const port1 = node1.ports[idPort1]
|
||||||
const node2 = this.stagedNodes[idNode2]
|
const node2 = this.stagedNodes[idNode2]
|
||||||
@@ -310,10 +334,12 @@ class BZgraflow extends Buildoz{
|
|||||||
}
|
}
|
||||||
const bb1 = port1.el.getBoundingClientRect()
|
const bb1 = port1.el.getBoundingClientRect()
|
||||||
const bb2 = port2.el.getBoundingClientRect()
|
const bb2 = port2.el.getBoundingClientRect()
|
||||||
const x1 = Math.floor(bb1.x + (bb1.width/2)) - svgRect.left
|
const p1 = this.clientToSvg(bb1.x + (bb1.width/2), bb1.y + (bb1.height/2))
|
||||||
const y1 = Math.floor(bb1.y + (bb1.height/2)) - svgRect.top
|
const p2 = this.clientToSvg(bb2.x + (bb2.width/2), bb2.y + (bb2.height/2))
|
||||||
const x2 = Math.floor(bb2.x + (bb2.width/2)) - svgRect.left
|
const x1 = Math.floor(p1.x)
|
||||||
const y2 = Math.floor(bb2.y + (bb2.height/2)) - svgRect.top
|
const y1 = Math.floor(p1.y)
|
||||||
|
const x2 = Math.floor(p2.x)
|
||||||
|
const y2 = Math.floor(p2.y)
|
||||||
const loop = (idNode1==idNode2) && (idPort1==idPort2)
|
const loop = (idNode1==idNode2) && (idPort1==idPort2)
|
||||||
const dist = Math.abs(x2 - x1) + Math.abs(y2 - y1)
|
const dist = Math.abs(x2 - x1) + Math.abs(y2 - y1)
|
||||||
|
|
||||||
@@ -336,7 +362,6 @@ class BZgraflow extends Buildoz{
|
|||||||
|
|
||||||
bezierInterNodes(idNode1, idPort1, idNode2, idPort2, interNodes, orientation='horizontal', tension=60) {
|
bezierInterNodes(idNode1, idPort1, idNode2, idPort2, interNodes, orientation='horizontal', tension=60) {
|
||||||
tension = parseInt(tension)
|
tension = parseInt(tension)
|
||||||
const svgRect = this.wiresContainer.getBoundingClientRect()
|
|
||||||
const node1 = this.stagedNodes[idNode1]
|
const node1 = this.stagedNodes[idNode1]
|
||||||
let port1 = node1.ports[idPort1]
|
let port1 = node1.ports[idPort1]
|
||||||
|
|
||||||
@@ -365,18 +390,23 @@ class BZgraflow extends Buildoz{
|
|||||||
const endPath = directPath.substring(directPath.lastIndexOf(',')+1).trim()
|
const endPath = directPath.substring(directPath.lastIndexOf(',')+1).trim()
|
||||||
let path = startPath
|
let path = startPath
|
||||||
let [ , x1, y1] = startPath.split(' ')
|
let [ , x1, y1] = startPath.split(' ')
|
||||||
x1 = parseInt(x1)
|
x1 = parseFloat(x1)
|
||||||
y1 = parseInt(y1)
|
y1 = parseFloat(y1)
|
||||||
for(const interNode of interNodes){
|
for(const interNode of interNodes){
|
||||||
const bb = this.stagedNodes[interNode].getBoundingClientRect()
|
const bb = this.stagedNodes[interNode].getBoundingClientRect()
|
||||||
let x2; let y2;
|
// Entry/exit points on the placeholder box, converted to SVG coords (handles CSS transforms)
|
||||||
|
let entryClient; let exitClient
|
||||||
if(orientation=='horizontal'){
|
if(orientation=='horizontal'){
|
||||||
x2 = bb.x -svgRect.left
|
entryClient = { x: bb.left, y: bb.top + (bb.height/2) }
|
||||||
y2 =Math.floor(bb.y + (bb.height/2)) - svgRect.top
|
exitClient = { x: bb.right, y: bb.top + (bb.height/2) }
|
||||||
} else {
|
} else {
|
||||||
x2 = Math.floor(bb.x + (bb.width/2)) - svgRect.left
|
entryClient = { x: bb.left + (bb.width/2), y: bb.top }
|
||||||
y2 = bb.y - svgRect.top
|
exitClient = { x: bb.left + (bb.width/2), y: bb.bottom }
|
||||||
}
|
}
|
||||||
|
const entry = this.clientToSvg(entryClient.x, entryClient.y)
|
||||||
|
const exit = this.clientToSvg(exitClient.x, exitClient.y)
|
||||||
|
let x2 = Math.floor(entry.x)
|
||||||
|
let y2 = Math.floor(entry.y)
|
||||||
|
|
||||||
if(port1){
|
if(port1){
|
||||||
path += makeCubicBezier(x1, y1, x2, y2, ['w','e'].includes(port1.direction) ? 'horizontal' : 'vertical', orientation)
|
path += makeCubicBezier(x1, y1, x2, y2, ['w','e'].includes(port1.direction) ? 'horizontal' : 'vertical', orientation)
|
||||||
@@ -385,26 +415,21 @@ class BZgraflow extends Buildoz{
|
|||||||
path += makeCubicBezier(x1, y1, x2, y2, orientation, orientation)
|
path += makeCubicBezier(x1, y1, x2, y2, orientation, orientation)
|
||||||
}
|
}
|
||||||
|
|
||||||
if(orientation=='horizontal'){
|
const x3 = Math.floor(exit.x)
|
||||||
x2 += bb.width
|
const y3 = Math.floor(exit.y)
|
||||||
path += ` L ${x2} ${y2} `
|
path += ` L ${x3} ${y3} `
|
||||||
} else {
|
|
||||||
y2 += bb.height
|
|
||||||
path += ` L ${x2} ${y2} `
|
|
||||||
}
|
|
||||||
|
|
||||||
x1 = x2
|
x1 = x3
|
||||||
y1 = y2
|
y1 = y3
|
||||||
}
|
}
|
||||||
let [x2, y2] = endPath.split(' ')
|
let [x2, y2] = endPath.split(' ')
|
||||||
x2 = parseInt(x2)
|
x2 = parseFloat(x2)
|
||||||
y2 = parseInt(y2)
|
y2 = parseFloat(y2)
|
||||||
path += ' '+makeCubicBezier(x1, y1, x2, y2, orientation, orientation)
|
path += ' '+makeCubicBezier(x1, y1, x2, y2, orientation, orientation)
|
||||||
return(path)
|
return(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
autoPlace(orientation = 'horizontal', gapx = 80, gapy = 80, tween=1000, align='center'){
|
autoPlace(orientation = 'horizontal', gapx = 80, gapy = 80, tween=500, align='center'){
|
||||||
console.log('autoPlace', orientation, gapx, gapy, tween, align)
|
|
||||||
// Loops create infinite recursion in dfs for getting parents & adjacency lists: Remove them !
|
// Loops create infinite recursion in dfs for getting parents & adjacency lists: Remove them !
|
||||||
let linksWithoutBackEdges
|
let linksWithoutBackEdges
|
||||||
if(this.hasAnyLoop(this.flow.nodes, this.flow.links)){
|
if(this.hasAnyLoop(this.flow.nodes, this.flow.links)){
|
||||||
@@ -425,9 +450,12 @@ class BZgraflow extends Buildoz{
|
|||||||
for(const layer of layers){
|
for(const layer of layers){
|
||||||
let totHeight = 0; let totWidth = 0
|
let totHeight = 0; let totWidth = 0
|
||||||
for(const [idx, nid] of layer.entries()){
|
for(const [idx, nid] of layer.entries()){
|
||||||
|
// Use offset* (not impacted by CSS transforms) to keep autoPlace stable during zoom animations.
|
||||||
const bb = this.stagedNodes[nid].getBoundingClientRect()
|
const bb = this.stagedNodes[nid].getBoundingClientRect()
|
||||||
totHeight += bb.height + gapy
|
const h = this.stagedNodes[nid].offsetHeight || bb.height
|
||||||
totWidth += bb.width + gapx
|
const w = this.stagedNodes[nid].offsetWidth || bb.width
|
||||||
|
totHeight += h + gapy
|
||||||
|
totWidth += w + gapx
|
||||||
indexes[nid] = { base: idx, ports: this.computePortOffsets(nid, orientation) }
|
indexes[nid] = { base: idx, ports: this.computePortOffsets(nid, orientation) }
|
||||||
}
|
}
|
||||||
if(totHeight>maxHeight) maxHeight = totHeight
|
if(totHeight>maxHeight) maxHeight = totHeight
|
||||||
@@ -490,7 +518,7 @@ class BZgraflow extends Buildoz{
|
|||||||
if(!nid.startsWith('longLinkPlaceHolder_')) {
|
if(!nid.startsWith('longLinkPlaceHolder_')) {
|
||||||
const bb = this.stagedNodes[nid].getBoundingClientRect()
|
const bb = this.stagedNodes[nid].getBoundingClientRect()
|
||||||
this.moveNode(nid, x, y, orientation, tween)
|
this.moveNode(nid, x, y, orientation, tween)
|
||||||
y += gapy + bb.height
|
y += gapy + (this.stagedNodes[nid].offsetHeight || bb.height)
|
||||||
} else {
|
} else {
|
||||||
this.addFakeNode(nid, x, y, wMax*0.75, fakeNodeHeight)
|
this.addFakeNode(nid, x, y, wMax*0.75, fakeNodeHeight)
|
||||||
this.moveNode(nid, x, y, orientation, tween)
|
this.moveNode(nid, x, y, orientation, tween)
|
||||||
@@ -509,7 +537,7 @@ class BZgraflow extends Buildoz{
|
|||||||
if(!nid.startsWith('longLinkPlaceHolder_')){
|
if(!nid.startsWith('longLinkPlaceHolder_')){
|
||||||
const bb = this.stagedNodes[nid].getBoundingClientRect()
|
const bb = this.stagedNodes[nid].getBoundingClientRect()
|
||||||
this.moveNode(nid, x, y, orientation, tween)
|
this.moveNode(nid, x, y, orientation, tween)
|
||||||
x += gapx + bb.width
|
x += gapx + (this.stagedNodes[nid].offsetWidth || bb.width)
|
||||||
} else {
|
} else {
|
||||||
this.addFakeNode(nid, x, y, fakeNodeWidth, hMax*0.75)
|
this.addFakeNode(nid, x, y, fakeNodeWidth, hMax*0.75)
|
||||||
this.moveNode(nid, x, y, orientation, tween)
|
this.moveNode(nid, x, y, orientation, tween)
|
||||||
@@ -524,7 +552,8 @@ class BZgraflow extends Buildoz{
|
|||||||
getMaxWidth(layer){
|
getMaxWidth(layer){
|
||||||
return(layer.filter(nid =>
|
return(layer.filter(nid =>
|
||||||
!nid.startsWith('longLinkPlaceHolder_'))
|
!nid.startsWith('longLinkPlaceHolder_'))
|
||||||
.map(nid => this.stagedNodes[nid].getBoundingClientRect().width)
|
// Use offsetWidth (not impacted by CSS transforms) to keep autoPlace stable during zoom animations.
|
||||||
|
.map(nid => (this.stagedNodes[nid].offsetWidth || this.stagedNodes[nid].getBoundingClientRect().width))
|
||||||
.reduce((a, b) => a > b ? a : b, 0)
|
.reduce((a, b) => a > b ? a : b, 0)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -532,7 +561,8 @@ class BZgraflow extends Buildoz{
|
|||||||
getMaxHeight(layer){
|
getMaxHeight(layer){
|
||||||
return(layer.filter(nid =>
|
return(layer.filter(nid =>
|
||||||
!nid.startsWith('longLinkPlaceHolder_'))
|
!nid.startsWith('longLinkPlaceHolder_'))
|
||||||
.map(nid => this.stagedNodes[nid].getBoundingClientRect().height)
|
// Use offsetHeight (not impacted by CSS transforms) to keep autoPlace stable during zoom animations.
|
||||||
|
.map(nid => (this.stagedNodes[nid].offsetHeight || this.stagedNodes[nid].getBoundingClientRect().height))
|
||||||
.reduce((a, b) => a > b ? a : b, 0)
|
.reduce((a, b) => a > b ? a : b, 0)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user