graflow: entering subflow ok
This commit is contained in:
142
bzGraflow.js
142
bzGraflow.js
@@ -65,13 +65,6 @@ class BZgraflow extends Buildoz{
|
||||
right: -1em;
|
||||
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.nodesContainer = document.createElement('div')
|
||||
@@ -159,18 +152,18 @@ class BZgraflow extends Buildoz{
|
||||
this.stagedNodes[id].dataset.id = id
|
||||
this.stagedNodes[id].ports = Object.fromEntries(Array.from(portEls).map(item => ([item.dataset.id, { ...item.dataset, el:item }])))
|
||||
if(node.subflow) {
|
||||
const btnZoomIn = document.createElement('button')
|
||||
btnZoomIn.classList.add('bzgf-zoom-in', 'icon-copy')
|
||||
btnZoomIn.addEventListener('click', () => {
|
||||
this.zoomIn(id)
|
||||
const btnEnterSubflow = document.createElement('button')
|
||||
btnEnterSubflow.classList.add('bzgf-zoom-in', 'icon-copy')
|
||||
btnEnterSubflow.addEventListener('click', () => {
|
||||
this.EnterSubflow(id)
|
||||
})
|
||||
this.stagedNodes[id].appendChild(btnZoomIn)
|
||||
this.stagedNodes[id].appendChild(btnEnterSubflow)
|
||||
}
|
||||
this.nodesContainer.append(this.stagedNodes[id])
|
||||
return(this.stagedNodes[id])
|
||||
}
|
||||
|
||||
zoomIn(id){
|
||||
EnterSubflow(id){
|
||||
const nodeEl = this.stagedNodes[id]
|
||||
if(!nodeEl) return
|
||||
|
||||
@@ -185,10 +178,17 @@ class BZgraflow extends Buildoz{
|
||||
const childEl = document.createElement('bz-graflow')
|
||||
childEl.setAttribute('flow', flowUrl)
|
||||
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)
|
||||
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
|
||||
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)
|
||||
childEl.style.transformOrigin = 'top left'
|
||||
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.setProperty('--tx', tx0 + 'px')
|
||||
childEl.style.setProperty('--ty', ty0 + 'px')
|
||||
@@ -213,24 +213,24 @@ class BZgraflow extends Buildoz{
|
||||
childEl.style.setProperty('--ty', '0px')
|
||||
childEl.style.setProperty('--sx', 1)
|
||||
childEl.style.setProperty('--sy', 1)
|
||||
this.hostContainer.style.opacity = '0'
|
||||
})
|
||||
|
||||
childEl.addEventListener('transitionend', (e) => {
|
||||
if(e.propertyName !== 'transform') return
|
||||
this.style.visibility = 'hidden'
|
||||
this.hostContainer.style.visibility = 'hidden'
|
||||
}, { once:true })
|
||||
}
|
||||
|
||||
Invade(oldEl, newEl, { hideOld=true } = {}){
|
||||
Invade(oldEl, newEl){
|
||||
const r = oldEl.getBoundingClientRect()
|
||||
if(hideOld) oldEl.style.visibility = 'hidden'
|
||||
newEl.style.position = 'fixed'
|
||||
newEl.style.left = r.left + 'px'
|
||||
newEl.style.top = r.top + 'px'
|
||||
newEl.style.width = r.width + 'px'
|
||||
newEl.style.height = r.height + 'px'
|
||||
newEl.style.display = 'block'
|
||||
oldEl.parentNode.appendChild(newEl)
|
||||
oldEl.appendChild(newEl)
|
||||
}
|
||||
|
||||
Evade(oldEl, newEl){
|
||||
@@ -291,15 +291,39 @@ class BZgraflow extends Buildoz{
|
||||
|
||||
if(forceAutoplace){
|
||||
const bb=this.getBoundingClientRect()
|
||||
//TODO compute tensions from ports
|
||||
if(bb.width > bb.height) this.autoPlace('horizontal', 80, 30, 500)
|
||||
else this.autoPlace('vertical', 80, 30, 200)
|
||||
//TODO compute tensions from ports, height and width
|
||||
if(bb.width > bb.height) this.autoPlace('horizontal', 60, 60)
|
||||
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) {
|
||||
tension = parseInt(tension)
|
||||
const svgRect = this.wiresContainer.getBoundingClientRect()
|
||||
const node1 = this.stagedNodes[idNode1]
|
||||
const port1 = node1.ports[idPort1]
|
||||
const node2 = this.stagedNodes[idNode2]
|
||||
@@ -310,10 +334,12 @@ class BZgraflow extends Buildoz{
|
||||
}
|
||||
const bb1 = port1.el.getBoundingClientRect()
|
||||
const bb2 = port2.el.getBoundingClientRect()
|
||||
const x1 = Math.floor(bb1.x + (bb1.width/2)) - svgRect.left
|
||||
const y1 = Math.floor(bb1.y + (bb1.height/2)) - svgRect.top
|
||||
const x2 = Math.floor(bb2.x + (bb2.width/2)) - svgRect.left
|
||||
const y2 = Math.floor(bb2.y + (bb2.height/2)) - svgRect.top
|
||||
const p1 = this.clientToSvg(bb1.x + (bb1.width/2), bb1.y + (bb1.height/2))
|
||||
const p2 = this.clientToSvg(bb2.x + (bb2.width/2), bb2.y + (bb2.height/2))
|
||||
const x1 = Math.floor(p1.x)
|
||||
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 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) {
|
||||
tension = parseInt(tension)
|
||||
const svgRect = this.wiresContainer.getBoundingClientRect()
|
||||
const node1 = this.stagedNodes[idNode1]
|
||||
let port1 = node1.ports[idPort1]
|
||||
|
||||
@@ -365,18 +390,23 @@ class BZgraflow extends Buildoz{
|
||||
const endPath = directPath.substring(directPath.lastIndexOf(',')+1).trim()
|
||||
let path = startPath
|
||||
let [ , x1, y1] = startPath.split(' ')
|
||||
x1 = parseInt(x1)
|
||||
y1 = parseInt(y1)
|
||||
x1 = parseFloat(x1)
|
||||
y1 = parseFloat(y1)
|
||||
for(const interNode of interNodes){
|
||||
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'){
|
||||
x2 = bb.x -svgRect.left
|
||||
y2 =Math.floor(bb.y + (bb.height/2)) - svgRect.top
|
||||
entryClient = { x: bb.left, y: bb.top + (bb.height/2) }
|
||||
exitClient = { x: bb.right, y: bb.top + (bb.height/2) }
|
||||
} else {
|
||||
x2 = Math.floor(bb.x + (bb.width/2)) - svgRect.left
|
||||
y2 = bb.y - svgRect.top
|
||||
entryClient = { x: bb.left + (bb.width/2), y: bb.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){
|
||||
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)
|
||||
}
|
||||
|
||||
if(orientation=='horizontal'){
|
||||
x2 += bb.width
|
||||
path += ` L ${x2} ${y2} `
|
||||
} else {
|
||||
y2 += bb.height
|
||||
path += ` L ${x2} ${y2} `
|
||||
}
|
||||
const x3 = Math.floor(exit.x)
|
||||
const y3 = Math.floor(exit.y)
|
||||
path += ` L ${x3} ${y3} `
|
||||
|
||||
x1 = x2
|
||||
y1 = y2
|
||||
x1 = x3
|
||||
y1 = y3
|
||||
}
|
||||
let [x2, y2] = endPath.split(' ')
|
||||
x2 = parseInt(x2)
|
||||
y2 = parseInt(y2)
|
||||
x2 = parseFloat(x2)
|
||||
y2 = parseFloat(y2)
|
||||
path += ' '+makeCubicBezier(x1, y1, x2, y2, orientation, orientation)
|
||||
return(path)
|
||||
}
|
||||
|
||||
autoPlace(orientation = 'horizontal', gapx = 80, gapy = 80, tween=1000, align='center'){
|
||||
console.log('autoPlace', orientation, gapx, gapy, tween, align)
|
||||
autoPlace(orientation = 'horizontal', gapx = 80, gapy = 80, tween=500, align='center'){
|
||||
// Loops create infinite recursion in dfs for getting parents & adjacency lists: Remove them !
|
||||
let linksWithoutBackEdges
|
||||
if(this.hasAnyLoop(this.flow.nodes, this.flow.links)){
|
||||
@@ -425,9 +450,12 @@ class BZgraflow extends Buildoz{
|
||||
for(const layer of layers){
|
||||
let totHeight = 0; let totWidth = 0
|
||||
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()
|
||||
totHeight += bb.height + gapy
|
||||
totWidth += bb.width + gapx
|
||||
const h = this.stagedNodes[nid].offsetHeight || bb.height
|
||||
const w = this.stagedNodes[nid].offsetWidth || bb.width
|
||||
totHeight += h + gapy
|
||||
totWidth += w + gapx
|
||||
indexes[nid] = { base: idx, ports: this.computePortOffsets(nid, orientation) }
|
||||
}
|
||||
if(totHeight>maxHeight) maxHeight = totHeight
|
||||
@@ -488,9 +516,9 @@ class BZgraflow extends Buildoz{
|
||||
}
|
||||
for(const nid of layer){
|
||||
if(!nid.startsWith('longLinkPlaceHolder_')) {
|
||||
const bb = this.stagedNodes[nid].getBoundingClientRect()
|
||||
const bb = this.stagedNodes[nid].getBoundingClientRect()
|
||||
this.moveNode(nid, x, y, orientation, tween)
|
||||
y += gapy + bb.height
|
||||
y += gapy + (this.stagedNodes[nid].offsetHeight || bb.height)
|
||||
} else {
|
||||
this.addFakeNode(nid, x, y, wMax*0.75, fakeNodeHeight)
|
||||
this.moveNode(nid, x, y, orientation, tween)
|
||||
@@ -509,7 +537,7 @@ class BZgraflow extends Buildoz{
|
||||
if(!nid.startsWith('longLinkPlaceHolder_')){
|
||||
const bb = this.stagedNodes[nid].getBoundingClientRect()
|
||||
this.moveNode(nid, x, y, orientation, tween)
|
||||
x += gapx + bb.width
|
||||
x += gapx + (this.stagedNodes[nid].offsetWidth || bb.width)
|
||||
} else {
|
||||
this.addFakeNode(nid, x, y, fakeNodeWidth, hMax*0.75)
|
||||
this.moveNode(nid, x, y, orientation, tween)
|
||||
@@ -524,7 +552,8 @@ class BZgraflow extends Buildoz{
|
||||
getMaxWidth(layer){
|
||||
return(layer.filter(nid =>
|
||||
!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)
|
||||
)
|
||||
}
|
||||
@@ -532,7 +561,8 @@ class BZgraflow extends Buildoz{
|
||||
getMaxHeight(layer){
|
||||
return(layer.filter(nid =>
|
||||
!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)
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user