better layout complete events
This commit is contained in:
+81
-30
@@ -518,8 +518,12 @@ class BZgraflow extends Buildoz{
|
||||
else this._scheduleLayoutWhenReady(() => this.autofit(autofitPercent))
|
||||
}
|
||||
}
|
||||
if(forceAutoplace) this._scheduleAutoPlaceWhenReady(this.currentOrientation, gapx, gapy, finishRefresh)
|
||||
else finishRefresh()
|
||||
const onLayoutComplete = () => {
|
||||
this.fireEvent('layoutComplete', { })
|
||||
finishRefresh()
|
||||
}
|
||||
if(forceAutoplace) this._scheduleAutoPlaceWhenReady(this.currentOrientation, gapx, gapy, onLayoutComplete)
|
||||
else onLayoutComplete()
|
||||
}
|
||||
|
||||
disconnectedCallback(){
|
||||
@@ -580,13 +584,20 @@ class BZgraflow extends Buildoz{
|
||||
requestAnimationFrame(rafPoll)
|
||||
}
|
||||
|
||||
_scheduleAutoPlaceWhenReady(orientation, gapx, gapy, onDone){
|
||||
_scheduleAutoPlaceWhenReady(orientation, gapx, gapy, onLayoutComplete){
|
||||
this._scheduleLayoutWhenReady(() => {
|
||||
this.autoPlace(orientation, gapx, gapy)
|
||||
if(typeof onDone === 'function') onDone()
|
||||
this.autoPlace(orientation, gapx, gapy, null, null, onLayoutComplete)
|
||||
})
|
||||
}
|
||||
|
||||
_maybeFireLayoutComplete(){
|
||||
if(this._layoutMovePending > 0) return
|
||||
if(!this._layoutCompleteHandler) return
|
||||
const fn = this._layoutCompleteHandler
|
||||
this._layoutCompleteHandler = null
|
||||
fn()
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -774,7 +785,7 @@ class BZgraflow extends Buildoz{
|
||||
return(path)
|
||||
}
|
||||
|
||||
autoPlace(orientation = null, gapx = null, gapy = null, tween = null, align = null){
|
||||
autoPlace(orientation = null, gapx = null, gapy = null, tween = null, align = null, onLayoutComplete = null){
|
||||
if(orientation == null) orientation = this.getBZAttribute('orientation') || this.currentOrientation || 'horizontal'
|
||||
if(gapx == null) gapx = parseInt(this.getBZAttribute('gapx')) || 80
|
||||
if(gapy == null) gapy = parseInt(this.getBZAttribute('gapy')) || 80
|
||||
@@ -786,6 +797,9 @@ class BZgraflow extends Buildoz{
|
||||
// moveNode() checks this token each frame and will no-op if superseded.
|
||||
this._autoPlaceToken = (this._autoPlaceToken || 0) + 1
|
||||
const token = this._autoPlaceToken
|
||||
this._layoutMovePending = 0
|
||||
this._layoutCompleteToken = token
|
||||
this._layoutCompleteHandler = (typeof onLayoutComplete === 'function') ? onLayoutComplete : null
|
||||
|
||||
// Cleanup placeholders from previous autoPlace() runs.
|
||||
// Each run creates new longLinkPlaceHolder_* IDs; without cleanup they accumulate in the DOM.
|
||||
@@ -909,7 +923,7 @@ class BZgraflow extends Buildoz{
|
||||
y = Math.max(parentsY[parents[nid][0]], y) //TODO handle multiple parents with avg
|
||||
}
|
||||
placedY = y
|
||||
this.moveNode(nid, x, y, orientation, tween, null, token)
|
||||
this.moveNode(nid, x, y, orientation, tween, token)
|
||||
if((align == 'parent') && (nid in parents) && (parents[nid][0] in parentsY)) {
|
||||
parentsY[parents[nid][0]] += gapy + nodeHeight
|
||||
} else {
|
||||
@@ -922,7 +936,7 @@ class BZgraflow extends Buildoz{
|
||||
}
|
||||
placedY = y
|
||||
this.addFakeNode(nid, x, y, wMax*0.75, fakeNodeHeight)
|
||||
this.moveNode(nid, x, y, orientation, tween, null, token)
|
||||
this.moveNode(nid, x, y, orientation, tween, token)
|
||||
// Never increment parentsY for fake nodes: they're placeholders and must not disalign real children
|
||||
y = Math.max(y, placedY + gapy + fakeNodeHeight)
|
||||
}
|
||||
@@ -943,7 +957,7 @@ class BZgraflow extends Buildoz{
|
||||
!nid.startsWith('longLinkPlaceHolder_') && nid in parents && parents[nid][0] === pid
|
||||
)
|
||||
if(firstRealChild && nodeY[pid] !== nodeY[firstRealChild]){
|
||||
this.moveNode(pid, nodeX[pid], nodeY[firstRealChild], orientation, tween, null, token)
|
||||
this.moveNode(pid, nodeX[pid], nodeY[firstRealChild], orientation, tween, token)
|
||||
nodeY[pid] = nodeY[firstRealChild]
|
||||
}
|
||||
}
|
||||
@@ -972,17 +986,18 @@ class BZgraflow extends Buildoz{
|
||||
for(const nid of layer){
|
||||
if(!nid.startsWith('longLinkPlaceHolder_')){
|
||||
const bb = this.stagedNodes[nid].getBoundingClientRect()
|
||||
this.moveNode(nid, x, y, orientation, tween, null, token)
|
||||
this.moveNode(nid, x, y, orientation, tween, token)
|
||||
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, null, token)
|
||||
this.moveNode(nid, x, y, orientation, tween, token)
|
||||
x += gapx + fakeNodeWidth
|
||||
}
|
||||
}
|
||||
y += hMax + gapy
|
||||
}
|
||||
}
|
||||
this._maybeFireLayoutComplete()
|
||||
}
|
||||
|
||||
clearFakeNodes(){
|
||||
@@ -1102,17 +1117,34 @@ class BZgraflow extends Buildoz{
|
||||
}
|
||||
|
||||
moveNode(nid, destx, desty, orientation, duration = 200, autoPlaceToken = null) {
|
||||
const t0 = performance.now()
|
||||
const el0 = this.stagedNodes?.[nid]
|
||||
if(!el0) return
|
||||
let layoutTracked = false
|
||||
if(autoPlaceToken != null && autoPlaceToken === this._layoutCompleteToken) {
|
||||
layoutTracked = true
|
||||
this._layoutMovePending++
|
||||
}
|
||||
const finishLayoutMove = () => {
|
||||
if(!layoutTracked) return
|
||||
layoutTracked = false
|
||||
this._layoutMovePending = Math.max(0, this._layoutMovePending - 1)
|
||||
this._maybeFireLayoutComplete()
|
||||
}
|
||||
const t0 = performance.now()
|
||||
const bb = el0.getBoundingClientRect()
|
||||
const parentbb = el0.parentElement.getBoundingClientRect()
|
||||
const x0=bb.x - parentbb.x
|
||||
const y0 = bb.y - parentbb.y
|
||||
function frame(t) {
|
||||
if(autoPlaceToken && autoPlaceToken !== this._autoPlaceToken) return
|
||||
if(autoPlaceToken && autoPlaceToken !== this._autoPlaceToken) {
|
||||
finishLayoutMove()
|
||||
return
|
||||
}
|
||||
const el = this.stagedNodes?.[nid]
|
||||
if(!el) return
|
||||
if(!el) {
|
||||
finishLayoutMove()
|
||||
return
|
||||
}
|
||||
const p = Math.min((t - t0) / duration, 1)
|
||||
const k = p * p * (3 - 2 * p) // smoothstep
|
||||
const x = x0 + (destx - x0) * k
|
||||
@@ -1130,6 +1162,7 @@ class BZgraflow extends Buildoz{
|
||||
flowNode.coords.y = y
|
||||
}
|
||||
this.fireEvent('nodeMoved', { nid, x, y })
|
||||
finishLayoutMove()
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(frame.bind(this))
|
||||
@@ -1299,15 +1332,11 @@ class BZgraflow extends Buildoz{
|
||||
return(crossLayerLinks)
|
||||
}
|
||||
|
||||
autofit(percent=100){
|
||||
if(!this.parentElement) return
|
||||
|
||||
const prevTransformOrigin = this.style.transformOrigin
|
||||
this.style.transform = 'none'
|
||||
this.style.transformOrigin = 'top left'
|
||||
|
||||
// Measure real content by unioning viewport-space bounding boxes.
|
||||
// This is robust with overflow:auto and absolute-positioned layers.
|
||||
/**
|
||||
* Union bounding boxes of nodes and wires (viewport coords).
|
||||
* Same measurement strategy as autofit(). Call with transform cleared for stable sizes.
|
||||
*/
|
||||
getContentSize(){
|
||||
let left = Infinity
|
||||
let top = Infinity
|
||||
let right = -Infinity
|
||||
@@ -1324,18 +1353,40 @@ class BZgraflow extends Buildoz{
|
||||
this.nodesContainer?.querySelectorAll?.('.bzgf-node').forEach(nodeEl => includeBB(nodeEl.getBoundingClientRect()))
|
||||
this.wiresContainer?.querySelectorAll?.('path.bzgf-wire').forEach(path => includeBB(path.getBoundingClientRect()))
|
||||
|
||||
const parentBB = this.parentElement.getBoundingClientRect()
|
||||
const gapx = parseInt(this.getBZAttribute('gapx')) || 80
|
||||
const gapy = parseInt(this.getBZAttribute('gapy')) || 80
|
||||
const rawW = Number.isFinite(left) && Number.isFinite(right) ? Math.max(right - left, 1) : Math.max(this.mainContainer?.clientWidth || this.offsetWidth || 1, 1)
|
||||
const rawH = Number.isFinite(top) && Number.isFinite(bottom) ? Math.max(bottom - top, 1) : Math.max(this.mainContainer?.clientHeight || this.offsetHeight || 1, 1)
|
||||
const contentW = rawW + (2 * gapx)
|
||||
const contentH = rawH + (2 * gapy)
|
||||
const hasBounds = Number.isFinite(left) && Number.isFinite(right) && Number.isFinite(top) && Number.isFinite(bottom)
|
||||
const rawWidth = hasBounds ? Math.max(right - left, 1) : Math.max(this.mainContainer?.clientWidth || this.offsetWidth || 1, 1)
|
||||
const rawHeight = hasBounds ? Math.max(bottom - top, 1) : Math.max(this.mainContainer?.clientHeight || this.offsetHeight || 1, 1)
|
||||
|
||||
return({
|
||||
left: hasBounds ? left : null,
|
||||
top: hasBounds ? top : null,
|
||||
right: hasBounds ? right : null,
|
||||
bottom: hasBounds ? bottom : null,
|
||||
rawWidth,
|
||||
rawHeight,
|
||||
width: rawWidth + (2 * gapx),
|
||||
height: rawHeight + (2 * gapy),
|
||||
gapx,
|
||||
gapy,
|
||||
})
|
||||
}
|
||||
|
||||
autofit(percent=100){
|
||||
if(!this.parentElement) return
|
||||
|
||||
const prevTransformOrigin = this.style.transformOrigin
|
||||
this.style.transform = 'none'
|
||||
this.style.transformOrigin = 'top left'
|
||||
|
||||
const { left, top, width: contentW, height: contentH, gapx, gapy } = this.getContentSize()
|
||||
const parentBB = this.parentElement.getBoundingClientRect()
|
||||
const sx = parentBB.width / contentW
|
||||
const sy = parentBB.height / contentH
|
||||
const scale = Math.min(sx, sy)*(percent/100) // uniform scale to fit inside parent
|
||||
const tx = Number.isFinite(left) ? (-left + gapx) : gapx
|
||||
const ty = Number.isFinite(top) ? (-top + gapy) : gapy
|
||||
const tx = left != null ? (-left + gapx) : gapx
|
||||
const ty = top != null ? (-top + gapy) : gapy
|
||||
if(!this.isSubflow) {
|
||||
this.style.transformOrigin = prevTransformOrigin || 'top left'
|
||||
this.style.transform = `scale(${scale}) translate(${tx}px, ${ty}px)`
|
||||
|
||||
Reference in New Issue
Block a user