From 5de769918293a85f8d4969419f9798f46236debf Mon Sep 17 00:00:00 2001 From: STEINNI Date: Sun, 15 Mar 2026 16:48:37 +0000 Subject: [PATCH 01/36] graflow: tempwire on creating new wire --- bzGraflow.js | 94 +++++++++++++++++---- graflow_examples/flows/testFlowEvent1.json | 98 ++++++++++++++++++++++ graflow_examples/test3.1.html | 98 ++++++++++++++++++++++ graflow_examples/test4.html | 1 + 4 files changed, 275 insertions(+), 16 deletions(-) create mode 100644 graflow_examples/flows/testFlowEvent1.json create mode 100644 graflow_examples/test3.1.html diff --git a/bzGraflow.js b/bzGraflow.js index 616af67..7f5b05b 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -644,11 +644,11 @@ class BZgraflow extends Buildoz{ return(path) } - autoPlace(orientation = 'horizontal', gapx = null, gapy = null, tween = null, align = null){ - if(gapx == null) gapx = parseInt(this.getBZAttribute('gapx')) || 80 - if(gapy == null) gapy = parseInt(this.getBZAttribute('gapy')) || 80 - if(tween == null) tween = parseInt(this.getBZAttribute('tween')) || 500 - if(align == null) align = this.getBZAttribute('align') || 'center' + autoPlace(orientation = 'horizontal', gapx = null, gapy = null, tween = null, align = null){ + if(gapx == null) gapx = parseInt(this.getBZAttribute('gapx')) || 80 + if(gapy == null) gapy = parseInt(this.getBZAttribute('gapy')) || 80 + if(tween == null) tween = parseInt(this.getBZAttribute('tween')) || 500 + if(align == null) align = this.getBZAttribute('align') || 'center' this.currentOrientation = orientation // Cancel any previous autoPlace() animations by bumping a token. @@ -795,7 +795,7 @@ class BZgraflow extends Buildoz{ this.moveNode(nid, x, y, orientation, tween, null, token) // Never increment parentsY for fake nodes: they're placeholders and must not disalign real children y = Math.max(y, placedY + gapy + fakeNodeHeight) - } + } parentsY[nid] = placedY nodeY[nid] = placedY nodeX[nid] = x @@ -1182,12 +1182,12 @@ class BZgraflow extends Buildoz{ Buildoz.define('graflow', BZgraflow) class MovingNodes{ + constructor(graflow, itemSelector, handleSelector = itemSelector){ this.graflow = graflow this.itemSelector = itemSelector this.handleSelector = handleSelector this.nodesContainer = this.graflow.mainContainer.querySelector('.bzgf-nodes-container') - this.state = null this.interactiveElementsSelector = ` .port, @@ -1197,6 +1197,9 @@ class MovingNodes{ button, a[href] ` + this._boundPointerDown = this.pointerDown.bind(this) + this._boundPointerMove = this.pointerMove.bind(this) + this._boundPointerUp = this.pointerUp.bind(this) this.graflow.addEventListener('refreshed', this.enableMovingNodes.bind(this)) } @@ -1210,21 +1213,21 @@ class MovingNodes{ } this.nodesContainer.querySelectorAll(this.handleSelector).forEach(item => - item.addEventListener('pointerdown', this.pointerDown.bind(this)) + item.addEventListener('pointerdown', this._boundPointerDown) ) - this.nodesContainer.addEventListener('pointermove', this.pointerMove.bind(this)) + this.nodesContainer.addEventListener('pointermove', this._boundPointerMove) this.nodesContainer.querySelectorAll(this.handleSelector).forEach(item => - item.addEventListener('pointerup', this.pointerUp.bind(this)) + item.addEventListener('pointerup', this._boundPointerUp) ) } disableMovingNodes(){ this.nodesContainer.querySelectorAll(this.handleSelector).forEach(item => - item.removeEventListener('pointerdown', this.pointerDown.bind(this)) + item.removeEventListener('pointerdown', this._boundPointerDown) ) - this.nodesContainer.removeEventListener('pointermove', this.pointerMove.bind(this)) + this.nodesContainer.removeEventListener('pointermove', this._boundPointerMove) this.nodesContainer.querySelectorAll(this.handleSelector).forEach(item => - item.removeEventListener('pointerup', this.pointerUp.bind(this)) + item.removeEventListener('pointerup', this._boundPointerUp) ) } @@ -1291,7 +1294,7 @@ class EditWires{ this.nodesContainer = this.graflow.mainContainer.querySelector('.bzgf-nodes-container') this.state = null this.graflow.tabIndex = 0 // Make keyboard reactive - + this._boundPointerMove = this.pointerMove.bind(this) this.graflow.addEventListener('refreshed', this.enableEditWires.bind(this)) this.graflow.addEventListener('refreshed', this.enableSelectPorts.bind(this)) this.graflow.addEventListener('wiresUpdated', this.enableEditWires.bind(this)) @@ -1322,19 +1325,68 @@ class EditWires{ if(this.currentlySelectedPort == port) { this.currentlySelectedPort.style.removeProperty('border') this.currentlySelectedPort = null + this.state = null + this.graflow.wiresContainer.removeEventListener('pointermove', this._boundPointerMove) + if(this.tempwire) this.tempwire.remove() return } if(this.currentlySelectedPort) { + this.tempwire.remove() + this.tempwire = null this.makeWireBetweenPorts(this.currentlySelectedPort, port) this.enableEditWires() this.currentlySelectedPort.style.removeProperty('border') this.currentlySelectedPort = null + this.state = null + this.graflow.wiresContainer.removeEventListener('pointermove', this._boundPointerMove) } else { + this.tension = parseInt(this.graflow.getBZAttribute('tension')) || 60 + this.wireType = this.graflow.getBZAttribute('wiretype') || 'bezier' this.currentlySelectedPort = port port.style.setProperty('border', '5px solid #FF0', 'important') + this.state = { + startX: e.clientX, + startY: e.clientY, + port + } + this.tempwire = document.createElementNS('http://www.w3.org/2000/svg', 'path') + this.graflow.wiresContainer.appendChild(this.tempwire) + this.tempwire.classList.add('bzgf-wire') + this.graflow.wiresContainer.addEventListener('pointermove', this._boundPointerMove) } } + + pointerMove(e){ + if(!this.state) return + const { port } = this.state + const bb = port.getBoundingClientRect() + const p1 = this.graflow.clientToSvg(bb.x + bb.width / 2, bb.y + bb.height / 2) + const p2 = this.graflow.clientToSvg(e.clientX, e.clientY) + 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 dir = port.dataset.direction + const c1x = x1 + this.tension * this.graflow.dirVect[dir].x + const c1y = y1 + this.tension * this.graflow.dirVect[dir].y + const c2x = x2 - this.tension * this.graflow.dirVect[dir].x + const c2y = y2 - this.tension * this.graflow.dirVect[dir].y + const node1 = port.closest('.bzgf-node') + const node2 = { offsetWidth: 0, offsetHeight: 0 } //Fake it for buildsegment + const seg = this.graflow.buildSegment( + x1, y1, + c1x, c1y, + c2x, c2y, + x2, y2, + this.wireType, + node1, node2, + dir, dir, + this.tension) + if(!seg) return + this.tempwire.setAttribute('d', `M ${x1} ${y1} ${seg}`) + } + makeWireBetweenPorts(port1, port2){ const node1 = port1.closest('.bzgf-node') const node2 = port2.closest('.bzgf-node') @@ -1370,8 +1422,18 @@ class EditWires{ return } if(e.key == 'Escape') { - if(this.currentlySelectedWire) this.currentlySelectedWire.style.setProperty('stroke', '#0000', 'important') - this.currentlySelectedWire = null + if(this.currentlySelectedWire) { + this.currentlySelectedWire.style.setProperty('stroke', '#0000', 'important') + this.currentlySelectedWire = null + } + if(this.currentlySelectedPort) { + this.currentlySelectedPort.style.removeProperty('border') + this.currentlySelectedPort = null + } + if(this.tempwire) { + this.tempwire.remove() + this.tempwire = null + } return } } diff --git a/graflow_examples/flows/testFlowEvent1.json b/graflow_examples/flows/testFlowEvent1.json new file mode 100644 index 0000000..caf880a --- /dev/null +++ b/graflow_examples/flows/testFlowEvent1.json @@ -0,0 +1,98 @@ +{ + "nodesFile": "./nodesLib/nodesEIC.html", + "flow": { + "nodes":[ + { "nodeType": "eicBasic", + "id": "config", + "ncoords": { "x": 50, "y": 120}, + "markup": { + "title": "Configure event", + "subtitle": "", + "severity": "warning" + } + }, + { "nodeType": "eicBasic", + "id": "mailinvit", + "ncoords": { "x": 100, "y": 220}, + "markup": { + "title": "Candidates invitation mailing", + "subtitle": "", + "severity": "success" + } + }, + { "nodeType": "eicBasic", + "id": "applicationsurvey", + "ncoords": { "x": 150, "y": 320}, + "markup": { + "title": "Application survey", + "subtitle": "", + "severity": "success" + } + }, + { "nodeType": "eicBasic", + "id": "enrollment", + "ncoords": { "x": 150, "y": 320}, + "markup": { + "title": "Candidates enrollment", + "subtitle": "", + "severity": "secondary" + } + }, + { "nodeType": "eicBasic", + "id": "mailwelcome", + "ncoords": { "x": 150, "y": 320}, + "markup": { + "title": "Welcome mailing", + "subtitle": "", + "severity": "warning" + } + }, + { "nodeType": "eicBasic", + "id": "mailrejection", + "ncoords": { "x": 150, "y": 320}, + "markup": { + "title": "Rejection mailing", + "subtitle": "", + "severity": "warning" + } + }, + { "nodeType": "eicBasic", + "id": "event", + "ncoords": { "x": 150, "y": 320}, + "markup": { + "title": "Event", + "subtitle": "", + "severity": "secondary" + } + }, + { "nodeType": "eicBasic", + "id": "mailstatisfaction", + "ncoords": { "x": 150, "y": 320}, + "markup": { + "title": "Satisfaction survey mailing", + "subtitle": "", + "severity": "success" + } + }, + { "nodeType": "eicBasic", + "id": "satisfactionsurvey", + "ncoords": { "x": 150, "y": 320}, + "markup": { + "title": "Satisfaction survey", + "subtitle": "", + "severity": "warning" + } + } + ], + "links": [ + { "from": ["config", "out1"], "to": ["mailinvit", "in1"] }, + { "from": ["mailinvit", "out1"], "to": ["applicationsurvey", "in1"] }, + { "from": ["applicationsurvey", "out1"], "to": ["enrollment", "in1"] }, + { "from": ["enrollment", "out1"], "to": ["mailwelcome", "in1"] }, + { "from": ["enrollment", "out1"], "to": ["mailrejection", "in1"] }, + { "from": ["mailwelcome", "out1"], "to": ["event", "in1"] }, + { "from": ["event", "out1"], "to": ["mailstatisfaction", "in1"] }, + { "from": ["mailstatisfaction", "out1"], "to": ["satisfactionsurvey", "in1"] } + ] + } +} \ No newline at end of file diff --git a/graflow_examples/test3.1.html b/graflow_examples/test3.1.html new file mode 100644 index 0000000..54fc996 --- /dev/null +++ b/graflow_examples/test3.1.html @@ -0,0 +1,98 @@ + + + + graflow + + + + + + + + + + + + +
+ + + + +
+
+
+ + diff --git a/graflow_examples/test4.html b/graflow_examples/test4.html index 40209c3..a1985cc 100644 --- a/graflow_examples/test4.html +++ b/graflow_examples/test4.html @@ -118,6 +118,7 @@ setTimeout(sevanimation, 1000) } grflw4.addEventListener('refreshed',() => { + if(aifmi) return aifmi = grflw4.stagedNodes['aifm-investment'].querySelector('.body') sevanimation() }) From 46a344db740ffd0d2251ab93f3c259257a4f46f7 Mon Sep 17 00:00:00 2001 From: STEINNI Date: Sun, 15 Mar 2026 18:04:51 +0000 Subject: [PATCH 02/36] graflow: improved landing of temp-wire on hovering dest port + improved test5 example to test it --- bzGraflow.js | 46 ++++++++++++++++++----- graflow_examples/flows/testFlow1.json | 4 ++ graflow_examples/nodesLib/nodesTest1.html | 23 ++++++++++++ 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/bzGraflow.js b/bzGraflow.js index 7f5b05b..4659363 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -1313,12 +1313,23 @@ class EditWires{ } enableSelectPorts(){ + this.currentlyHoveredPort = null const portEls = this.graflow.nodesContainer.querySelectorAll('.port') for(const port of portEls){ port.addEventListener('click', this.onSelectPort.bind(this)) + port.addEventListener('pointerenter', this._onPortPointerEnter.bind(this)) + port.addEventListener('pointerleave', this._onPortPointerLeave.bind(this)) port.classList.add('selectable') } } + + _onPortPointerEnter(e){ + this.currentlyHoveredPort = e.target.closest('.port') + } + + _onPortPointerLeave(e){ + if(this.currentlyHoveredPort === e.target.closest('.port')) this.currentlyHoveredPort = null + } onSelectPort(e){ const port = e.target @@ -1326,7 +1337,8 @@ class EditWires{ this.currentlySelectedPort.style.removeProperty('border') this.currentlySelectedPort = null this.state = null - this.graflow.wiresContainer.removeEventListener('pointermove', this._boundPointerMove) + this._setWirecoatsPointerEvents('') + this.graflow.mainContainer.removeEventListener('pointermove', this._boundPointerMove) if(this.tempwire) this.tempwire.remove() return } @@ -1338,7 +1350,8 @@ class EditWires{ this.currentlySelectedPort.style.removeProperty('border') this.currentlySelectedPort = null this.state = null - this.graflow.wiresContainer.removeEventListener('pointermove', this._boundPointerMove) + this._setWirecoatsPointerEvents('') + this.graflow.mainContainer.removeEventListener('pointermove', this._boundPointerMove) } else { this.tension = parseInt(this.graflow.getBZAttribute('tension')) || 60 this.wireType = this.graflow.getBZAttribute('wiretype') || 'bezier' @@ -1350,12 +1363,20 @@ class EditWires{ port } this.tempwire = document.createElementNS('http://www.w3.org/2000/svg', 'path') + this.tempwire.setAttribute('fill', 'none') + this.tempwire.style.pointerEvents = 'none' this.graflow.wiresContainer.appendChild(this.tempwire) this.tempwire.classList.add('bzgf-wire') - this.graflow.wiresContainer.addEventListener('pointermove', this._boundPointerMove) + this._setWirecoatsPointerEvents('none') + this.graflow.mainContainer.addEventListener('pointermove', this._boundPointerMove) } } + _setWirecoatsPointerEvents(value){ + this.graflow.wiresContainer.querySelectorAll('.bzgf-wirecoat').forEach(el => { el.style.pointerEvents = value }) + } + +//TODO: Check if autoplace sees the wiring changes ! pointerMove(e){ if(!this.state) return @@ -1367,13 +1388,17 @@ class EditWires{ const y1 = Math.floor(p1.y) const x2 = Math.floor(p2.x) const y2 = Math.floor(p2.y) - const dir = port.dataset.direction - const c1x = x1 + this.tension * this.graflow.dirVect[dir].x - const c1y = y1 + this.tension * this.graflow.dirVect[dir].y - const c2x = x2 - this.tension * this.graflow.dirVect[dir].x - const c2y = y2 - this.tension * this.graflow.dirVect[dir].y + const dir1 = port.dataset.direction + const oppositeDir = { n: 's', s: 'n', e: 'w', w: 'e' } + const hovered = this.currentlyHoveredPort + + const dir2 = (hovered && hovered !== port) ? hovered.dataset.direction : oppositeDir[dir1] + const c1x = x1 + this.tension * this.graflow.dirVect[dir1].x + const c1y = y1 + this.tension * this.graflow.dirVect[dir1].y + const c2x = x2 + this.tension * this.graflow.dirVect[dir2].x + const c2y = y2 + this.tension * this.graflow.dirVect[dir2].y const node1 = port.closest('.bzgf-node') - const node2 = { offsetWidth: 0, offsetHeight: 0 } //Fake it for buildsegment + const node2 = hovered?.closest('.bzgf-node') ?? { offsetWidth: 0, offsetHeight: 0 } const seg = this.graflow.buildSegment( x1, y1, c1x, c1y, @@ -1381,7 +1406,7 @@ class EditWires{ x2, y2, this.wireType, node1, node2, - dir, dir, + dir1, dir2, this.tension) if(!seg) return this.tempwire.setAttribute('d', `M ${x1} ${y1} ${seg}`) @@ -1433,6 +1458,7 @@ class EditWires{ if(this.tempwire) { this.tempwire.remove() this.tempwire = null + this.graflow.mainContainer.removeEventListener('pointermove', this._boundPointerMove) } return } diff --git a/graflow_examples/flows/testFlow1.json b/graflow_examples/flows/testFlow1.json index 04a49a1..4c76651 100644 --- a/graflow_examples/flows/testFlow1.json +++ b/graflow_examples/flows/testFlow1.json @@ -33,6 +33,10 @@ { "nodeType": "console", "id": "9999", "coords": { "x": 800, "y": 350} + }, + { "nodeType": "square", + "id": "prng", + "coords": { "x": 250, "y": 400} } ], "links": [ diff --git a/graflow_examples/nodesLib/nodesTest1.html b/graflow_examples/nodesLib/nodesTest1.html index 07533b6..fd7e277 100644 --- a/graflow_examples/nodesLib/nodesTest1.html +++ b/graflow_examples/nodesLib/nodesTest1.html @@ -96,6 +96,16 @@ .bzgf-node[data-nodetype="input"] .title, .bzgf-node[data-nodetype="console"] .title{ background: #555; } + + + .bzgf-node[data-nodetype="square"]{ + background: #FAA; + border-color: #A00; + width: 100px; + height: 100px; + } + .bzgf-node[data-nodetype="square"] .title{ background: #555; } + .bzgf-node[data-nodetype="refnodein"], .bzgf-node[data-nodetype="refnodeout"] { width:3em; height:3em; @@ -188,6 +198,19 @@ + + + From c3624d8aca107978afe5bd09324d7e1626bf835f Mon Sep 17 00:00:00 2001 From: STEINNI Date: Sun, 15 Mar 2026 18:54:14 +0000 Subject: [PATCH 04/36] graflow: autoplace uses live links --- bzGraflow.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/bzGraflow.js b/bzGraflow.js index 14c938e..6f340ce 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -659,17 +659,17 @@ class BZgraflow extends Buildoz{ // Cleanup placeholders from previous autoPlace() runs. // Each run creates new longLinkPlaceHolder_* IDs; without cleanup they accumulate in the DOM. this.clearFakeNodes() - + let links = Object.values(this.stagedWires).map(w => w?.link).filter(Boolean) + links = links.length ? links : (this.flow?.links || []) // Loops create infinite recursion in dfs for getting parents & adjacency lists: Remove them ! let linksWithoutBackEdges - if(this.hasAnyLoop(this.flow.nodes, this.flow.links)){ - const backEdges = this.findBackEdges(this.flow.nodes, this.flow.links) - linksWithoutBackEdges = this.flow.links.filter((link, idx) => (!backEdges.includes(idx)) && (link.from[0] != link.to[0])) + if(this.hasAnyLoop(this.flow.nodes, links)){ + const backEdges = this.findBackEdges(this.flow.nodes, links) + linksWithoutBackEdges = links.filter((link, idx) => (!backEdges.includes(idx)) && (link.from[0] != link.to[0])) } else { - linksWithoutBackEdges = this.flow.links + linksWithoutBackEdges = links } const { parents, adj } = this.buildGraphStructures(this.flow.nodes, linksWithoutBackEdges) -console.log('parents', parents) const layers = this.computeLayers(this.flow.nodes, parents) // Layer-0 nodes have no parents, so reorderLayers() (which uses parent ordering) cannot @@ -721,7 +721,7 @@ console.log('parents', parents) // If any long-links, create placeholders for skipped layers this._virtualLinks = new Map() - this.flow.longLinks = this.findLongLinks(this.flow.links) + this.flow.longLinks = this.findLongLinks(links) for(const llink of this.flow.longLinks){ let fakeParent = llink.link.from[0] for(const layerIdx of llink.skippedLayers){ @@ -1138,11 +1138,11 @@ console.log('parents', parents) findLongLinks(links) { let linksWithoutBackEdges - if(this.hasAnyLoop(this.flow.nodes, this.flow.links)){ - const backEdges = this.findBackEdges(this.flow.nodes, this.flow.links) - linksWithoutBackEdges = this.flow.links.filter((link, idx) => (!backEdges.includes(idx)) && (link.from[0] != link.to[0])) + if(this.hasAnyLoop(this.flow.nodes, links)){ + const backEdges = this.findBackEdges(this.flow.nodes, links) + linksWithoutBackEdges = links.filter((link, idx) => (!backEdges.includes(idx)) && (link.from[0] != link.to[0])) } else { - linksWithoutBackEdges = this.flow.links + linksWithoutBackEdges = links } /// Yes that means we ignore long & back links ! const { parents } = this.buildGraphStructures(this.flow.nodes, linksWithoutBackEdges) From e58ff43014e48e1121b47b2bb7d547f54b1def99 Mon Sep 17 00:00:00 2001 From: STEINNI Date: Sat, 21 Mar 2026 15:49:40 +0000 Subject: [PATCH 05/36] fireEvent + graflow uses new-style fireEvent --- buildoz.js | 10 ++++++ bzGraflow-editor.js | 35 +++++++++++++++++++ bzGraflow.js | 58 +++++++++---------------------- graflow_examples/etest1.html | 65 +++++++++++++++++++++++++++++++++++ graflow_examples/test.html | 29 ++++++++++++---- graflow_examples/test1.html | 4 +-- graflow_examples/test2.html | 4 +-- graflow_examples/test3.1.html | 2 +- graflow_examples/test3.html | 4 +-- graflow_examples/test4.5.html | 4 +-- graflow_examples/test4.html | 6 ++-- graflow_examples/test5.html | 4 +-- graflow_examples/test6.html | 4 +-- 13 files changed, 165 insertions(+), 64 deletions(-) create mode 100644 bzGraflow-editor.js create mode 100644 graflow_examples/etest1.html diff --git a/buildoz.js b/buildoz.js index 5718f0d..2e3695b 100644 --- a/buildoz.js +++ b/buildoz.js @@ -48,6 +48,16 @@ class Buildoz extends HTMLElement { getBZAttribute(attrName){ // Little helper for defaults return(this.getAttribute(attrName) || this.defaultAttrs[attrName] ) } + + fireEvent(eventName, detail){ + let myname = this.tagName.toLocaleLowerCase() + myname = myname.substring(myname.indexOf('-')+1) + this.dispatchEvent(new CustomEvent(`bz:${myname}:${eventName}`, { + detail, + bubbles: true, + composed: true, + })) + } } class BZselect extends Buildoz { diff --git a/bzGraflow-editor.js b/bzGraflow-editor.js new file mode 100644 index 0000000..442476d --- /dev/null +++ b/bzGraflow-editor.js @@ -0,0 +1,35 @@ +/** + * _ ___ Another + * / |/ (_)______ __ _____ + * / / / __(_- { + childEl.addEventListener('bz:graflow:flowLoaded', (e) => { for(const portLink of flowNode.subflow.portLinks){ const nid = crypto.randomUUID() childEl.addNode({ @@ -302,12 +294,8 @@ class BZgraflow extends Buildoz{ this.hostContainer.style.visibility = 'hidden' childEl.style.transform = 'none' // Important for nested subflows to position correctly childEl.style.willChange = '' - newEl.style.overflow = 'auto' - this.dispatchEvent(new CustomEvent('subflowLoaded', { - detail: { subflow: childEl }, - bubbles: true, - composed: true, - })) + childEl.style.overflow = 'auto' + this.fireEvent('subflowLoaded', { subflow: childEl }) }, { once:true }) } @@ -382,11 +370,7 @@ class BZgraflow extends Buildoz{ this.hostContainer.style.opacity = '1' this.hostContainer.style.visibility = 'visible' childEl.style.willChange = '' - this.dispatchEvent(new CustomEvent('subflowExited', { - detail: { subflow: childEl }, - bubbles: true, - composed: true, - })) + this.fireEvent('subflowExited', { subflow: childEl }) }, { once:true }) } @@ -450,11 +434,7 @@ class BZgraflow extends Buildoz{ else this.currentOrientation = 'vertical' } if(forceAutoplace) this.autoPlace(this.currentOrientation) - this.dispatchEvent(new CustomEvent('refreshed', { - detail: { }, - bubbles: true, - composed: true, - })) + this.fireEvent('refreshed', { }) } // Convert viewport (client) coordinates to this instance's SVG local coordinates. @@ -993,11 +973,7 @@ class BZgraflow extends Buildoz{ if(p < 1) requestAnimationFrame(frame.bind(this)) else{ - this.dispatchEvent(new CustomEvent('nodeMoved', { - detail: { nid, x, y }, - bubbles: true, - composed: true, - })) + this.fireEvent('nodeMoved', { nid, x, y }) } } requestAnimationFrame(frame.bind(this)) @@ -1021,11 +997,7 @@ class BZgraflow extends Buildoz{ wire.setAttribute('d', path) } } - this.dispatchEvent(new CustomEvent('wiresUpdated', { - detail: { nid, orientation, LondLinkfix }, - bubbles: true, - composed: true, - })) + this.fireEvent('wiresUpdated', { nid, orientation, LondLinkfix }) } getLink(nid1, nid2){ @@ -1200,7 +1172,7 @@ class MovingNodes{ this._boundPointerDown = this.pointerDown.bind(this) this._boundPointerMove = this.pointerMove.bind(this) this._boundPointerUp = this.pointerUp.bind(this) - this.graflow.addEventListener('refreshed', this.enableMovingNodes.bind(this)) + this.graflow.addEventListener('bz:graflow:refreshed', this.enableMovingNodes.bind(this)) } enableMovingNodes() { @@ -1285,6 +1257,8 @@ class MovingNodes{ this.state.node.releasePointerCapture(e.pointerId) this.state.node.style.pointerEvents = '' this.state = null + + this.graflow.fireEvent('nodeMoved', { nodeId: this.state.node.dataset.id, x: this.state.x, y: this.state.y }) } } @@ -1295,9 +1269,9 @@ class EditWires{ this.state = null this.graflow.tabIndex = 0 // Make keyboard reactive this._boundPointerMove = this.pointerMove.bind(this) - this.graflow.addEventListener('refreshed', this.enableEditWires.bind(this)) - this.graflow.addEventListener('refreshed', this.enableSelectPorts.bind(this)) - this.graflow.addEventListener('wiresUpdated', this.enableEditWires.bind(this)) + this.graflow.addEventListener('bz:graflow:refreshed', this.enableEditWires.bind(this)) + this.graflow.addEventListener('bz:graflow:refreshed', this.enableSelectPorts.bind(this)) + this.graflow.addEventListener('bz:graflow:wiresUpdated', this.enableEditWires.bind(this)) this.graflow.addEventListener('keyup', this.onKeyUp.bind(this)) } @@ -1422,6 +1396,7 @@ class EditWires{ return('') } this.graflow.addWire({ from: [idNode1, idPort1], to: [idNode2, idPort2] }) + this.graflow.fireEvent('wireAdded', { from: [idNode1, idPort1], to: [idNode2, idPort2], id: `${idNode1}_${idNode2}` }) } onSelectWire(e){ @@ -1446,6 +1421,7 @@ class EditWires{ delete this.graflow.stagedWires[wireId] this.currentlySelectedWire.remove() this.currentlySelectedWire = null + this.graflow.fireEvent('wireRemoved', { wireId }) return } if(e.key == 'Escape') { diff --git a/graflow_examples/etest1.html b/graflow_examples/etest1.html new file mode 100644 index 0000000..44d6dfa --- /dev/null +++ b/graflow_examples/etest1.html @@ -0,0 +1,65 @@ + + + + graflow + + + + + + + + + + + + + + + + diff --git a/graflow_examples/test.html b/graflow_examples/test.html index fddcd92..e4e6039 100644 --- a/graflow_examples/test.html +++ b/graflow_examples/test.html @@ -44,7 +44,7 @@ test1 - P42 + Graflow P42 YES 2 depths @@ -56,7 +56,7 @@ test2 - Organigram + Graflow Organigram YES @@ -68,7 +68,7 @@ test3 - EIC simple + Graflow EIC simple NO 1 depth @@ -80,7 +80,7 @@ test4 - EIC-ICMP + Graflow EIC-ICMP NO X @@ -92,7 +92,7 @@ test4.5 - EIC-ICMP II + Graflow EIC-ICMP II NO @@ -104,7 +104,7 @@ test5 - P42 + Graflow P42 YES @@ -116,7 +116,7 @@ test6 - 16 ports test + Graflow 16 ports test NO @@ -127,6 +127,21 @@ + +
+ + + + + + + + + + + + +
Style
Editor testP42
diff --git a/graflow_examples/test1.html b/graflow_examples/test1.html index 633b040..fc9f6d3 100644 --- a/graflow_examples/test1.html +++ b/graflow_examples/test1.html @@ -53,10 +53,10 @@ + + + + + + + + + + + + diff --git a/graflow_examples/test.html b/graflow_examples/test.html index 615b71e..1812668 100644 --- a/graflow_examples/test.html +++ b/graflow_examples/test.html @@ -134,13 +134,24 @@ Style + Import / Export - Editor test + Editor test 1 P42 + JSON local file + + Editor test 2 + EIC + API to MySQL + + From 90f45c2c8d8fde39b1ad9407f97f8b4893286234 Mon Sep 17 00:00:00 2001 From: STEINNI Date: Fri, 10 Apr 2026 12:49:39 +0000 Subject: [PATCH 13/36] graflow: orientation as param --- bzGraflow.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/bzGraflow.js b/bzGraflow.js index 3682346..d29acbe 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -264,7 +264,7 @@ class BZgraflow extends Buildoz{ } } - childEl.autoPlace(this.currentOrientation, 60, 60) + childEl.autoPlace(this.currentOrientation, parseInt(this.getBZAttribute('gapx')) || 80, parseInt(this.getBZAttribute('gapy')) || 80) }, { once:true }) // Fade out the current (host) graflow while the child scales up @@ -442,11 +442,16 @@ class BZgraflow extends Buildoz{ this.addWire(link) } if(!this.currentOrientation) { - const bb=this.getBoundingClientRect() - if(bb.width > bb.height) this.currentOrientation = 'horizontal' - else this.currentOrientation = 'vertical' + if(this.getBZAttribute('orientation')) { + this.currentOrientation = this.getBZAttribute('orientation') + } else { + const bb=this.getBoundingClientRect() + if(bb.width > bb.height) this.currentOrientation = 'horizontal' + else this.currentOrientation = 'vertical' + } } - if(forceAutoplace) this.autoPlace(this.currentOrientation) + console.log('forceAutoplace', forceAutoplace, this.currentOrientation) + if(forceAutoplace) this.autoPlace(this.currentOrientation, parseInt(this.getBZAttribute('gapx')) || 80, parseInt(this.getBZAttribute('gapy')) || 80) this.fireEvent('refreshed', { }) } From 511562c502a868452a6672379944ddac131bb785 Mon Sep 17 00:00:00 2001 From: STEINNI Date: Fri, 10 Apr 2026 13:31:39 +0000 Subject: [PATCH 14/36] graflow: loadNodes is triple modes now: url, blob or object --- bzGraflow.js | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/bzGraflow.js b/bzGraflow.js index d29acbe..665faeb 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -109,24 +109,32 @@ class BZgraflow extends Buildoz{ } async loadFlow(source){ - let buf + let buf, flowObj if(source instanceof Blob){ buf = await source.text() - } else { + try{ + flowObj = JSON.parse(buf) + } catch(err){ + this.error('Could not parse flow JSON!?', err) + return + } + } else if(typeof source == 'string') { const url = source const fetchUrl = (typeof url === 'string' && !url.startsWith('blob:') && !url.startsWith('data:')) ? (url + '?' + crypto.randomUUID()) : url const res = await fetch(fetchUrl) buf = await res.text() + try{ + flowObj = JSON.parse(buf) + } catch(err){ + this.error('Could not parse flow JSON!?', err) + return + } + } else if(typeof source == 'object') { + flowObj = source } - let flowObj - try{ - flowObj = JSON.parse(buf) - } catch(err){ - this.error('Could not parse flow JSON!?', err) - return - } + if(!flowObj.nodesFile){ this.error('No nodesFile in JSON!?') return @@ -450,7 +458,6 @@ class BZgraflow extends Buildoz{ else this.currentOrientation = 'vertical' } } - console.log('forceAutoplace', forceAutoplace, this.currentOrientation) if(forceAutoplace) this.autoPlace(this.currentOrientation, parseInt(this.getBZAttribute('gapx')) || 80, parseInt(this.getBZAttribute('gapy')) || 80) this.fireEvent('refreshed', { }) } From 38ed29327ea8fddaa33d1d5a6964e242b1ffc937 Mon Sep 17 00:00:00 2001 From: STEINNI Date: Fri, 10 Apr 2026 14:35:01 +0000 Subject: [PATCH 15/36] subflow can take js objects or url --- bzGraflow.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bzGraflow.js b/bzGraflow.js index 665faeb..1eb2c91 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -230,14 +230,18 @@ class BZgraflow extends Buildoz{ // scaled down so it fits exactly inside the clicked node, then animate it to full size. const nodeBB = nodeEl.getBoundingClientRect() const parentBB = this.getBoundingClientRect() - const flowNode = this.flow?.nodes?.find(n => n.id === id) - const flowUrl = flowNode.subflow.url const childEl = document.createElement('bz-graflow') childEl.isSubflow = true childEl.currentOrientation = this.currentOrientation - childEl.setAttribute('flow', flowUrl) + if(flowNode.subflow.url) childEl.setAttribute('flow', flowNode.subflow.url) + else { + childEl.addEventListener('bz:graflow:domConnected', (e) => { + e.detail.graflow.loadFlow(flowNode.subflow.flow) + }) + } + childEl.setAttribute('tension', this.getBZAttribute('tension') || '60') // Remember which node we "came from" so exitSubflow() can animate back to it. childEl.dataset.enterNodeId = id From 9d703f064001edfa20f34e38521b3ec628222781 Mon Sep 17 00:00:00 2001 From: STEINNI Date: Fri, 10 Apr 2026 14:50:54 +0000 Subject: [PATCH 16/36] graflow: subflow tweaks --- bzGraflow.js | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/bzGraflow.js b/bzGraflow.js index 1eb2c91..57e0d60 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -235,25 +235,6 @@ class BZgraflow extends Buildoz{ const childEl = document.createElement('bz-graflow') childEl.isSubflow = true childEl.currentOrientation = this.currentOrientation - if(flowNode.subflow.url) childEl.setAttribute('flow', flowNode.subflow.url) - else { - childEl.addEventListener('bz:graflow:domConnected', (e) => { - e.detail.graflow.loadFlow(flowNode.subflow.flow) - }) - } - - childEl.setAttribute('tension', this.getBZAttribute('tension') || '60') - // Remember which node we "came from" so exitSubflow() can animate back to it. - childEl.dataset.enterNodeId = id - const btnExitSubflow = document.createElement('button') - btnExitSubflow.classList.add('bzgf-zoom-out') - this.addIcon(btnExitSubflow, 'zoomout') - btnExitSubflow.addEventListener('click', () => { - this.exitSubflow(childEl) - }) - // Put the child in the exact same viewport rect as the parent - this.invade(this, childEl) - childEl.hostContainer.appendChild(btnExitSubflow) childEl.addEventListener('bz:graflow:flowLoaded', (e) => { for(const portLink of flowNode.subflow.portLinks){ @@ -279,6 +260,28 @@ class BZgraflow extends Buildoz{ childEl.autoPlace(this.currentOrientation, parseInt(this.getBZAttribute('gapx')) || 80, parseInt(this.getBZAttribute('gapy')) || 80) }, { once:true }) + if(flowNode.subflow.url) childEl.setAttribute('flow', flowNode.subflow.url) + else { + childEl.addEventListener('bz:graflow:domConnected', async (e) => { + await childEl.loadFlow(flowNode.subflow.flow) + }) + } + + childEl.setAttribute('tension', this.getBZAttribute('tension') || '60') + // Remember which node we "came from" so exitSubflow() can animate back to it. + childEl.dataset.enterNodeId = id + const btnExitSubflow = document.createElement('button') + btnExitSubflow.classList.add('bzgf-zoom-out') + this.addIcon(btnExitSubflow, 'zoomout') + btnExitSubflow.addEventListener('click', () => { + this.exitSubflow(childEl) + }) + // Put the child in the exact same viewport rect as the parent + this.invade(this, childEl) + childEl.hostContainer.appendChild(btnExitSubflow) + +///////////////////// + // 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' From 93ea54da575411c0c8a395827f0e15bb34fd6c58 Mon Sep 17 00:00:00 2001 From: STEINNI Date: Fri, 10 Apr 2026 15:19:21 +0000 Subject: [PATCH 17/36] graflow: adjust loadnodes from object with clone to avoid polution --- bzGraflow.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bzGraflow.js b/bzGraflow.js index 57e0d60..4ea2d49 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -132,7 +132,7 @@ class BZgraflow extends Buildoz{ return } } else if(typeof source == 'object') { - flowObj = source + flowObj = structuredClone(source) } if(!flowObj.nodesFile){ From 7090bf7a3a48b7bb5699719889c3756a433dccea Mon Sep 17 00:00:00 2001 From: STEINNI Date: Fri, 10 Apr 2026 16:40:31 +0000 Subject: [PATCH 18/36] graflow: fixed the underscore as separator in internal link storage --- bzGraflow.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/bzGraflow.js b/bzGraflow.js index 4ea2d49..ba3bb1e 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -414,6 +414,10 @@ class BZgraflow extends Buildoz{ return(this.stagedNodes[nid]) } + makeWireId(nid1, nid2){ + return(`${encodeURIComponent(nid1)}|${encodeURIComponent(nid2)}`) + } + addWire(link){ const [idNode1, idPort1] = link.from const [idNode2, idPort2] = link.to @@ -422,7 +426,7 @@ class BZgraflow extends Buildoz{ return } const path = this.linkNodes(idNode1, idPort1, idNode2, idPort2) - const id = `${idNode1}_${idNode2}` + const id = this.makeWireId(idNode1, idNode2) this.stagedWires[id] = document.createElementNS('http://www.w3.org/2000/svg', 'path') this.stagedWires[id].setAttribute('d', path) this.stagedWires[id].setAttribute('fill', 'none') @@ -1018,10 +1022,14 @@ class BZgraflow extends Buildoz{ updateWires(nid, orientation, LondLinkfix = false){ const wires = Object.keys(this.stagedWires) - .filter(id => (id.startsWith(nid+'_')||id.endsWith('_'+nid))) .map(id => this.stagedWires[id]) + .filter(wire => { + const lnk = wire?.link + return(lnk && (lnk.from?.[0] == nid || lnk.to?.[0] == nid)) + }) for(const wire of wires){ - const [nid1, nid2] = wire.dataset.id.split('_') + const nid1 = wire.link.from[0] + const nid2 = wire.link.to[0] const lnk = this.getLink(nid1, nid2) if(!lnk) continue if(!this.flow?.longLinks) this.flow.longLinks = [] @@ -1038,7 +1046,7 @@ class BZgraflow extends Buildoz{ } getLink(nid1, nid2){ - const wire = this.stagedWires[`${nid1}_${nid2}`] + const wire = this.stagedWires[this.makeWireId(nid1, nid2)] if(wire?.link) return wire.link return this._virtualLinks?.get(`${nid1}__${nid2}`) ?? null } @@ -1442,7 +1450,7 @@ class EditWires{ return('') } this.graflow.addWire({ from: [idNode1, idPort1], to: [idNode2, idPort2] }) - this.graflow.fireEvent('wireAdded', { from: [idNode1, idPort1], to: [idNode2, idPort2], id: `${idNode1}_${idNode2}` }) + this.graflow.fireEvent('wireAdded', { from: [idNode1, idPort1], to: [idNode2, idPort2], id: this.graflow.makeWireId(idNode1, idNode2) }) } onSelectWire(e){ @@ -1461,7 +1469,7 @@ class EditWires{ const wireId = this.currentlySelectedWire.dataset.id const linkToRemove = this.graflow.stagedWires[wireId]?.link this.graflow.flow.links = this.graflow.flow.links.filter(link => - linkToRemove ? link !== linkToRemove : (link.from[0] + '_' + link.to[0] !== wireId) + linkToRemove ? link !== linkToRemove : (this.graflow.makeWireId(link.from[0], link.to[0]) !== wireId) ) this.graflow.stagedWires[wireId]?.remove() delete this.graflow.stagedWires[wireId] From 88074b914404db8af716cc7a8e04c7c4d1ce3397 Mon Sep 17 00:00:00 2001 From: STEINNI Date: Mon, 13 Apr 2026 07:45:31 +0000 Subject: [PATCH 19/36] graflow: cleanup in subflow child params inheritance --- bzGraflow.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/bzGraflow.js b/bzGraflow.js index ba3bb1e..77be0bc 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -235,6 +235,10 @@ class BZgraflow extends Buildoz{ const childEl = document.createElement('bz-graflow') childEl.isSubflow = true childEl.currentOrientation = this.currentOrientation + const inheritedAttrs = ['orientation', 'gapx', 'gapy', 'tween', 'align', 'tension', 'wiretype', 'edit'] + for(const attrName of inheritedAttrs){ + if(this.hasAttribute(attrName)) childEl.setAttribute(attrName, this.getAttribute(attrName)) + } childEl.addEventListener('bz:graflow:flowLoaded', (e) => { for(const portLink of flowNode.subflow.portLinks){ @@ -257,7 +261,7 @@ class BZgraflow extends Buildoz{ } } - childEl.autoPlace(this.currentOrientation, parseInt(this.getBZAttribute('gapx')) || 80, parseInt(this.getBZAttribute('gapy')) || 80) + childEl.autoPlace() }, { once:true }) if(flowNode.subflow.url) childEl.setAttribute('flow', flowNode.subflow.url) @@ -280,8 +284,6 @@ class BZgraflow extends Buildoz{ this.invade(this, childEl) childEl.hostContainer.appendChild(btnExitSubflow) -///////////////////// - // 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' @@ -660,7 +662,8 @@ class BZgraflow extends Buildoz{ return(path) } - autoPlace(orientation = 'horizontal', gapx = null, gapy = null, tween = null, align = null){ + autoPlace(orientation = null, gapx = null, gapy = null, tween = null, align = 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 if(tween == null) tween = parseInt(this.getBZAttribute('tween')) || 500 From bf837e187be63497c2396afb31ce64db7d7317f7 Mon Sep 17 00:00:00 2001 From: STEINNI Date: Mon, 13 Apr 2026 07:45:53 +0000 Subject: [PATCH 20/36] graflow: cleanup in subflow child params inheritance --- bzGraflow.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bzGraflow.js b/bzGraflow.js index 77be0bc..60b6b0a 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -235,7 +235,7 @@ class BZgraflow extends Buildoz{ const childEl = document.createElement('bz-graflow') childEl.isSubflow = true childEl.currentOrientation = this.currentOrientation - const inheritedAttrs = ['orientation', 'gapx', 'gapy', 'tween', 'align', 'tension', 'wiretype', 'edit'] + const inheritedAttrs = ['orientation', 'gapx', 'gapy', 'tween', 'align', 'tension', 'wiretype', 'edit'] // ! Not 'isolated' ! for(const attrName of inheritedAttrs){ if(this.hasAttribute(attrName)) childEl.setAttribute(attrName, this.getAttribute(attrName)) } From e4c4bb7f861f99545c268eb8337afef1bc74a1c2 Mon Sep 17 00:00:00 2001 From: STEINNI Date: Mon, 13 Apr 2026 08:15:59 +0000 Subject: [PATCH 21/36] Graflow: subflow enter/exit tween aligned with tween param --- bzGraflow.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bzGraflow.js b/bzGraflow.js index 60b6b0a..cb1ca5b 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -308,7 +308,7 @@ class BZgraflow extends Buildoz{ // Force style flush, then animate back to identity (full parent size) childEl.getBoundingClientRect() - childEl.style.transition = 'transform 1000ms ease-in-out' + childEl.style.transition = `transform ${parseInt(this.getBZAttribute('tween')) || 500}ms ease-in-out` requestAnimationFrame(() => { childEl.style.top = 0; childEl.style.left = 0; From 0adc9666084d2d0808f91f732d1fb0b757482771 Mon Sep 17 00:00:00 2001 From: STEINNI Date: Mon, 13 Apr 2026 14:14:59 +0000 Subject: [PATCH 22/36] Graflow: autofit as atrtibute --- bzGraflow.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bzGraflow.js b/bzGraflow.js index cb1ca5b..c12c17a 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -235,7 +235,7 @@ class BZgraflow extends Buildoz{ const childEl = document.createElement('bz-graflow') childEl.isSubflow = true childEl.currentOrientation = this.currentOrientation - const inheritedAttrs = ['orientation', 'gapx', 'gapy', 'tween', 'align', 'tension', 'wiretype', 'edit'] // ! Not 'isolated' ! + const inheritedAttrs = ['orientation', 'gapx', 'gapy', 'tween', 'align', 'tension', 'wiretype', 'edit', 'autofit'] // ! Not 'isolated' ! for(const attrName of inheritedAttrs){ if(this.hasAttribute(attrName)) childEl.setAttribute(attrName, this.getAttribute(attrName)) } @@ -473,6 +473,13 @@ class BZgraflow extends Buildoz{ } if(forceAutoplace) this.autoPlace(this.currentOrientation, parseInt(this.getBZAttribute('gapx')) || 80, parseInt(this.getBZAttribute('gapy')) || 80) this.fireEvent('refreshed', { }) + if(this.hasAttribute('autofit')){ + const autofitAttr = this.getAttribute('autofit') + const autofitPercent = (autofitAttr !== null && autofitAttr !== '' && !Number.isNaN(parseFloat(autofitAttr))) + ? parseFloat(autofitAttr) + : undefined + this.autofit(autofitPercent) + } } // Convert viewport (client) coordinates to this instance's SVG local coordinates. From 54eb584fd7a2eb56b5e95a813a59155a4cd77252 Mon Sep 17 00:00:00 2001 From: STEINNI Date: Mon, 13 Apr 2026 14:33:57 +0000 Subject: [PATCH 23/36] Graflow: autofit fix for subflows to include refNodes --- bzGraflow.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bzGraflow.js b/bzGraflow.js index c12c17a..b33016e 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -260,8 +260,8 @@ class BZgraflow extends Buildoz{ }) } } - - childEl.autoPlace() + // Rebuild once refNodes are injected so the final refresh/autofit includes them. + childEl.refresh() }, { once:true }) if(flowNode.subflow.url) childEl.setAttribute('flow', flowNode.subflow.url) From 0e6d23c1e11145200f611cee03335e5aa256b97a Mon Sep 17 00:00:00 2001 From: STEINNI Date: Mon, 13 Apr 2026 15:14:00 +0000 Subject: [PATCH 24/36] Graflow: fixed autofit --- bzGraflow.js | 40 ++++++++++++++++++++++++++++++++----- graflow_examples/test4.html | 2 +- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/bzGraflow.js b/bzGraflow.js index b33016e..f3a6c50 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -1195,15 +1195,45 @@ class BZgraflow extends Buildoz{ } 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. + let left = Infinity + let top = Infinity + let right = -Infinity + let bottom = -Infinity + + const includeBB = (bb) => { + if(!bb) return + left = Math.min(left, bb.left) + top = Math.min(top, bb.top) + right = Math.max(right, bb.right) + bottom = Math.max(bottom, bb.bottom) + } + + 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() - // Use scroll dimensions for actual content extent (nodes can extend beyond element bounds) - const contentW = Math.max(this.scrollWidth || this.offsetWidth || 1, 1) - const contentH = Math.max(this.scrollHeight || this.offsetHeight || 1, 1) + 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 sx = parentBB.width / contentW const sy = parentBB.height / contentH const scale = Math.min(sx, sy)*(percent/100) // uniform scale to fit inside parent - this.style.transformOrigin = 'top left' - this.style.transform = `scale(${scale})` + const tx = Number.isFinite(left) ? (-left + gapx) : gapx + const ty = Number.isFinite(top) ? (-top + gapy) : gapy + this.style.transformOrigin = prevTransformOrigin || 'top left' + // First normalize content origin to (0,0), then scale to fit. + this.style.transform = `scale(${scale}) translate(${tx}px, ${ty}px)` } } Buildoz.define('graflow', BZgraflow) diff --git a/graflow_examples/test4.html b/graflow_examples/test4.html index 0d88e88..d1fc467 100644 --- a/graflow_examples/test4.html +++ b/graflow_examples/test4.html @@ -110,7 +110,7 @@ ro.observe(el) let aifmi = null; let sidx=0; - const sevanimation = () => { console.log('sevanimation') + const sevanimation = () => { severities.forEach(severity => aifmi.removeAttribute(severity)) aifmi.setAttribute(severities[sidx], '') sidx++ From 9381d82ae57e2e8e2c64d58c6630ba390c14edf7 Mon Sep 17 00:00:00 2001 From: STEINNI Date: Mon, 13 Apr 2026 15:21:39 +0000 Subject: [PATCH 25/36] Graflow: autofit removed translations --- bzGraflow.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/bzGraflow.js b/bzGraflow.js index f3a6c50..53ff9e9 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -1196,11 +1196,6 @@ class BZgraflow extends Buildoz{ 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. let left = Infinity @@ -1229,11 +1224,7 @@ class BZgraflow extends Buildoz{ 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 - this.style.transformOrigin = prevTransformOrigin || 'top left' - // First normalize content origin to (0,0), then scale to fit. - this.style.transform = `scale(${scale}) translate(${tx}px, ${ty}px)` + this.style.transform = `scale(${scale})` } } Buildoz.define('graflow', BZgraflow) From cfe33b81112ad2378fcecd2663ef19d4dabe02db Mon Sep 17 00:00:00 2001 From: STEINNI Date: Mon, 13 Apr 2026 15:23:15 +0000 Subject: [PATCH 26/36] Graflow: autofit readded translations --- bzGraflow.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/bzGraflow.js b/bzGraflow.js index 53ff9e9..f3a6c50 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -1196,6 +1196,11 @@ class BZgraflow extends Buildoz{ 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. let left = Infinity @@ -1224,7 +1229,11 @@ class BZgraflow extends Buildoz{ const sx = parentBB.width / contentW const sy = parentBB.height / contentH const scale = Math.min(sx, sy)*(percent/100) // uniform scale to fit inside parent - this.style.transform = `scale(${scale})` + const tx = Number.isFinite(left) ? (-left + gapx) : gapx + const ty = Number.isFinite(top) ? (-top + gapy) : gapy + this.style.transformOrigin = prevTransformOrigin || 'top left' + // First normalize content origin to (0,0), then scale to fit. + this.style.transform = `scale(${scale}) translate(${tx}px, ${ty}px)` } } Buildoz.define('graflow', BZgraflow) From 33ea2bd67258aa955b68ee17a35d613a7d9730a4 Mon Sep 17 00:00:00 2001 From: STEINNI Date: Mon, 13 Apr 2026 16:05:12 +0000 Subject: [PATCH 27/36] Graflow: dataset.subflow --- bzGraflow.js | 1 + 1 file changed, 1 insertion(+) diff --git a/bzGraflow.js b/bzGraflow.js index f3a6c50..44915ec 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -214,6 +214,7 @@ class BZgraflow extends Buildoz{ this.enterSubflow(id) }) this.stagedNodes[id].appendChild(btnEnterSubflow) + this.stagedNodes[id].dataset.subflow = true } this.nodesContainer.append(this.stagedNodes[id]) if(!this.flow.nodes.find(n => n.id === id)) { From eb891c8cefbffb3f26b637b8ff0529f322d640c5 Mon Sep 17 00:00:00 2001 From: STEINNI Date: Tue, 14 Apr 2026 10:40:08 +0000 Subject: [PATCH 28/36] Graflow: added parentNodeId to subflowLoaded event --- bzGraflow.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bzGraflow.js b/bzGraflow.js index 44915ec..10f9844 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -326,7 +326,7 @@ class BZgraflow extends Buildoz{ childEl.style.transform = 'none' // Important for nested subflows to position correctly childEl.style.willChange = '' childEl.style.overflow = 'auto' - this.fireEvent('subflowLoaded', { subflow: childEl }) + this.fireEvent('subflowLoaded', { parentNodeId: id, subflow: childEl }) }, { once:true }) } From f031142848eb998394816d6e574642b311f37f70 Mon Sep 17 00:00:00 2001 From: STEINNI Date: Tue, 14 Apr 2026 11:04:24 +0000 Subject: [PATCH 29/36] Graflow: more flexible markup in ref-nodes --- bzGraflow.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bzGraflow.js b/bzGraflow.js index 10f9844..4918cda 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -247,7 +247,7 @@ class BZgraflow extends Buildoz{ childEl.addNode({ "nodeType": portLink.refNodeType, "id": nid, - "markup": { "parentport": portLink.parentPort } + "markup": { ...portLink } }) if(portLink.direction=='in') { childEl.addWire({ From 5f8e3865e35e0a00fc2abbebe24bf36cd4f74fe6 Mon Sep 17 00:00:00 2001 From: STEINNI Date: Tue, 14 Apr 2026 14:10:15 +0000 Subject: [PATCH 30/36] Graflow: autofit fixes for subflows --- bzGraflow.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/bzGraflow.js b/bzGraflow.js index 4918cda..438f01a 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -272,7 +272,6 @@ class BZgraflow extends Buildoz{ }) } - childEl.setAttribute('tension', this.getBZAttribute('tension') || '60') // Remember which node we "came from" so exitSubflow() can animate back to it. childEl.dataset.enterNodeId = id const btnExitSubflow = document.createElement('button') @@ -1232,9 +1231,14 @@ class BZgraflow extends Buildoz{ 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 - this.style.transformOrigin = prevTransformOrigin || 'top left' - // First normalize content origin to (0,0), then scale to fit. - this.style.transform = `scale(${scale}) translate(${tx}px, ${ty}px)` + if(!this.isSubflow) { + this.style.transformOrigin = prevTransformOrigin || 'top left' + this.style.transform = `scale(${scale}) translate(${tx}px, ${ty}px)` + } else { + this.style.transform = `scale(${scale})` + this.style.width = `calc(100% / ${scale})` // means 100% of the parent node DESPITE the scaling + this.style.height = `calc(100% / ${scale})` // means 100% of the parent node DESPITE the scaling + } } } Buildoz.define('graflow', BZgraflow) From 3e7a82edc2f2c7403cbbc4568ea15015527916c6 Mon Sep 17 00:00:00 2001 From: STEINNI Date: Thu, 16 Apr 2026 08:03:32 +0000 Subject: [PATCH 31/36] Graflow: standardized event signatures between floLoaded and subflowLoaded --- bzGraflow.js | 10 ++++++++-- graflow_examples/test1.html | 2 +- graflow_examples/test2.html | 2 +- graflow_examples/test3.html | 2 +- graflow_examples/test4.5.html | 2 +- graflow_examples/test4.html | 2 +- graflow_examples/test5.html | 2 +- graflow_examples/test6.html | 2 +- 8 files changed, 15 insertions(+), 9 deletions(-) diff --git a/bzGraflow.js b/bzGraflow.js index 438f01a..4843902 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -142,7 +142,10 @@ class BZgraflow extends Buildoz{ await this.loadNodes(flowObj.nodesFile) this.flow = flowObj.flow this.refresh() - this.fireEvent('flowLoaded', { url: source instanceof Blob ? null : source, blob: source instanceof Blob ? source : null }) + this.fireEvent('flowLoaded', { + parentNodeId: null, + component: this, + }) } initFlow(){ @@ -325,7 +328,10 @@ class BZgraflow extends Buildoz{ childEl.style.transform = 'none' // Important for nested subflows to position correctly childEl.style.willChange = '' childEl.style.overflow = 'auto' - this.fireEvent('subflowLoaded', { parentNodeId: id, subflow: childEl }) + this.fireEvent('subflowLoaded', { + parentNodeId: id, + component: childEl + }) }, { once:true }) } diff --git a/graflow_examples/test1.html b/graflow_examples/test1.html index 41607a5..97232d1 100644 --- a/graflow_examples/test1.html +++ b/graflow_examples/test1.html @@ -54,7 +54,7 @@ window.addEventListener('load',()=>{ let grflw1 = document.querySelector('bz-graflow.compunet') grflw1.addEventListener('bz:graflow:subflowLoaded', - (evt) => { grflw1 = evt.detail.subflow } + (evt) => { grflw1 = evt.detail.component } ) grflw1.addEventListener('bz:graflow:subflowExited', (evt) => { grflw1 = evt.target } diff --git a/graflow_examples/test2.html b/graflow_examples/test2.html index 70d5263..b9e7039 100644 --- a/graflow_examples/test2.html +++ b/graflow_examples/test2.html @@ -54,7 +54,7 @@ window.addEventListener('load',()=>{ let grflw3 = document.querySelector('bz-graflow.organi') grflw3.addEventListener('bz:graflow:subflowLoaded', - (evt) => { grflw3 = evt.detail.subflow } + (evt) => { grflw3 = evt.detail.component } ) grflw3.addEventListener('bz:graflow:subflowExited', (evt) => { grflw3 = evt.target } diff --git a/graflow_examples/test3.html b/graflow_examples/test3.html index a14bc58..b7847bb 100644 --- a/graflow_examples/test3.html +++ b/graflow_examples/test3.html @@ -57,7 +57,7 @@ grflw2.setAttribute('align', document.querySelector('select[name="align"]').value) grflw2.setAttribute('wiretype', document.querySelector('select[name="wiretype"]').value) grflw2.addEventListener('bz:graflow:subflowLoaded', - (evt) => { grflw2 = evt.detail.subflow } + (evt) => { grflw2 = evt.detail.component } ) grflw2.addEventListener('bz:graflow:subflowExited', (evt) => { grflw2 = evt.target } diff --git a/graflow_examples/test4.5.html b/graflow_examples/test4.5.html index 474fc5b..a97e5e8 100644 --- a/graflow_examples/test4.5.html +++ b/graflow_examples/test4.5.html @@ -57,7 +57,7 @@ grflw4.setAttribute('align', document.querySelector('select[name="align"]').value) grflw4.setAttribute('wiretype', document.querySelector('select[name="wiretype"]').value) grflw4.addEventListener('bz:graflow:subflowLoaded', - (evt) => { grflw4 = evt.detail.subflow } + (evt) => { grflw4 = evt.detail.component } ) grflw4.addEventListener('bz:graflow:subflowExited', (evt) => { grflw4 = evt.target } diff --git a/graflow_examples/test4.html b/graflow_examples/test4.html index d1fc467..2afbc2e 100644 --- a/graflow_examples/test4.html +++ b/graflow_examples/test4.html @@ -83,7 +83,7 @@ let grflw4 = document.querySelector('bz-graflow.icmp') grflw4.addEventListener('bz:graflow:subflowLoaded', - (evt) => { grflw4 = evt.detail.subflow } + (evt) => { grflw4 = evt.detail.component } ) grflw4.addEventListener('bz:graflow:subflowExited', (evt) => { grflw4 = evt.target } diff --git a/graflow_examples/test5.html b/graflow_examples/test5.html index 4e583dc..7c41d9d 100644 --- a/graflow_examples/test5.html +++ b/graflow_examples/test5.html @@ -54,7 +54,7 @@ window.addEventListener('load',()=>{ let grflw1 = document.querySelector('bz-graflow.compunet') grflw1.addEventListener('bz:graflow:subflowLoaded', - (evt) => { grflw1 = evt.detail.subflow } + (evt) => { grflw1 = evt.detail.component } ) grflw1.addEventListener('bz:graflow:subflowExited', (evt) => { grflw1 = evt.target } diff --git a/graflow_examples/test6.html b/graflow_examples/test6.html index 3284210..0e022c5 100644 --- a/graflow_examples/test6.html +++ b/graflow_examples/test6.html @@ -57,7 +57,7 @@ grflw1.setAttribute('align', document.querySelector('select[name="align"]').value) grflw1.setAttribute('wiretype', document.querySelector('select[name="wiretype"]').value) grflw1.addEventListener('bz:graflow:subflowLoaded', - (evt) => { grflw1 = evt.detail.subflow } + (evt) => { grflw1 = evt.detail.component } ) grflw1.addEventListener('bz:graflow:subflowExited', (evt) => { grflw1 = evt.target } From 4f728a35146dc1a750259d7e092d06e53bbb9e8c Mon Sep 17 00:00:00 2001 From: STEINNI Date: Thu, 16 Apr 2026 09:15:11 +0000 Subject: [PATCH 32/36] Graflow: standardized event signatures between floLoaded and subflowLoaded and subflowExited --- bzGraflow.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bzGraflow.js b/bzGraflow.js index 4843902..ecde514 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -406,7 +406,9 @@ class BZgraflow extends Buildoz{ this.hostContainer.style.opacity = '1' this.hostContainer.style.visibility = 'visible' childEl.style.willChange = '' - this.fireEvent('subflowExited', { subflow: childEl }) + this.fireEvent('subflowExited', { + component: this + }) }, { once:true }) } From 605398505a377b08415abc244bca49ce9c51682c Mon Sep 17 00:00:00 2001 From: STEINNI Date: Tue, 21 Apr 2026 13:55:28 +0000 Subject: [PATCH 33/36] graflow: disabled attribute --- buildoz.css | 2 +- bzGraflow.js | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/buildoz.css b/buildoz.css index 367caee..275f81e 100644 --- a/buildoz.css +++ b/buildoz.css @@ -254,7 +254,7 @@ bz-graflow .bzgf-nodes-container{ /* used to keep the nodes container pointer-ev } bz-graflow .bzgf-nodes-container > * { /* allow the nodes to be moved ! */ pointer-events: auto; - } +} bz-graflow .bzgf-nodes-container .bzgf-node{ position:absolute; } bz-graflow .bzgf-nodes-container .bzgf-fake-node{ diff --git a/bzGraflow.js b/bzGraflow.js index ecde514..1aa6cff 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -98,6 +98,28 @@ class BZgraflow extends Buildoz{ else this.initFlow() } + static get observedAttributes(){ + return([...super.observedAttributes, 'disabled']) + } + + attributeChangedCallback(name, oldValue, newValue) { + super.attributeChangedCallback(name, oldValue, newValue) + if(name == 'disabled'){ + if(newValue === null) { + this.disabled = false + this.style.opacity = 1 + this.style.pointerEvents = 'auto' + } else { + this.disabled = true + this.style.opacity = 0.5 + this.style.pointerEvents = 'none' + } + this.querySelectorAll('.bzgf-zoom-in, .bzgf-zoom-out').forEach((btn) => { + btn.disabled = this.disabled + }) + } + } + error(msg, err){ this.querySelector('.graflow-error')?.remove() const errorEl = document.createElement('div') @@ -227,6 +249,7 @@ class BZgraflow extends Buildoz{ } enterSubflow(id){ + if(this.disabled || this.hasAttribute('disabled')) return const nodeEl = this.stagedNodes[id] if(!nodeEl) return From 0c21f7b7722a98c4378d41d521b9b453834b2963 Mon Sep 17 00:00:00 2001 From: STEINNI Date: Tue, 5 May 2026 13:26:29 +0000 Subject: [PATCH 34/36] Graflow: unique refs for arrows to fix arrows in subflows --- bzGraflow.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bzGraflow.js b/bzGraflow.js index 1aa6cff..289278a 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -30,6 +30,7 @@ class BZgraflow extends Buildoz{ this.stagedNodes = { } this.stagedWires = { } this.arrowDefs = null + this.arrowMarkerId = `arrow-${crypto.randomUUID()}` this.currentOrientation = null } @@ -184,6 +185,8 @@ class BZgraflow extends Buildoz{ for(const tpl of doc.querySelectorAll('template')){ if(tpl.id=='svg-arrows'){ this.arrowDefs = tpl.querySelector('defs').cloneNode(true) + const defaultArrow = this.arrowDefs.querySelector('#arrow') + if(defaultArrow) defaultArrow.id = this.arrowMarkerId this.wiresContainer.appendChild(this.arrowDefs) } else { const rootEl = tpl.content.querySelector('.bzgf-node') @@ -463,8 +466,8 @@ class BZgraflow extends Buildoz{ this.stagedWires[id] = document.createElementNS('http://www.w3.org/2000/svg', 'path') this.stagedWires[id].setAttribute('d', path) this.stagedWires[id].setAttribute('fill', 'none') - if(this.arrowDefs && link.endArrow) this.stagedWires[id].setAttribute('marker-end','url(#arrow)') - if(this.arrowDefs && link.startArrow) this.stagedWires[id].setAttribute('marker-start','url(#arrow)') + if(this.arrowDefs && link.endArrow) this.stagedWires[id].setAttribute('marker-end',`url(#${this.arrowMarkerId})`) + if(this.arrowDefs && link.startArrow) this.stagedWires[id].setAttribute('marker-start',`url(#${this.arrowMarkerId})`) this.stagedWires[id].classList.add('bzgf-wire') this.stagedWires[id].dataset.id = id this.stagedWires[id].link = link From 694e610ebb20a65e6808aeeb4520b4adb5390ff9 Mon Sep 17 00:00:00 2001 From: STEINNI Date: Fri, 22 May 2026 09:13:52 +0000 Subject: [PATCH 35/36] fixed bad autoplace if unfocused while autoplacing --- bzGraflow.js | 87 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 8 deletions(-) diff --git a/bzGraflow.js b/bzGraflow.js index 289278a..02fb486 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -505,15 +505,86 @@ class BZgraflow extends Buildoz{ else this.currentOrientation = 'vertical' } } - if(forceAutoplace) this.autoPlace(this.currentOrientation, parseInt(this.getBZAttribute('gapx')) || 80, parseInt(this.getBZAttribute('gapy')) || 80) - this.fireEvent('refreshed', { }) - if(this.hasAttribute('autofit')){ - const autofitAttr = this.getAttribute('autofit') - const autofitPercent = (autofitAttr !== null && autofitAttr !== '' && !Number.isNaN(parseFloat(autofitAttr))) - ? parseFloat(autofitAttr) - : undefined - this.autofit(autofitPercent) + const gapx = parseInt(this.getBZAttribute('gapx')) || 80 + const gapy = parseInt(this.getBZAttribute('gapy')) || 80 + const finishRefresh = () => { + this.fireEvent('refreshed', { }) + if(this.hasAttribute('autofit')){ + const autofitAttr = this.getAttribute('autofit') + const autofitPercent = (autofitAttr !== null && autofitAttr !== '' && !Number.isNaN(parseFloat(autofitAttr))) + ? parseFloat(autofitAttr) + : undefined + if(this._canRunAutoPlace()) this.autofit(autofitPercent) + else this._scheduleLayoutWhenReady(() => this.autofit(autofitPercent)) + } } + if(forceAutoplace) this._scheduleAutoPlaceWhenReady(this.currentOrientation, gapx, gapy, finishRefresh) + else finishRefresh() + } + + disconnectedCallback(){ + this._disconnectLayoutObserver() + super.disconnectedCallback?.() + } + + _disconnectLayoutObserver(){ + if(this._layoutObserver) { + this._layoutObserver.disconnect() + this._layoutObserver = null + } + } + + _canRunAutoPlace(){ + let nodesHaveLayoutSize = false + const ids = Object.keys(this.stagedNodes || {}) + if(ids.length === 0) return(true) + for(const nid of ids){ + if(nid.startsWith('longLinkPlaceHolder_')) continue + const el = this.stagedNodes[nid] + if(el && (el.offsetWidth > 0 || el.offsetHeight > 0)) { + nodesHaveLayoutSize=true + break + } + } + return((this.clientWidth > 0 && this.clientHeight > 0) && nodesHaveLayoutSize) + } + + /** + * autoPlace uses offsetWidth/offsetHeight; when the host is hidden (display:none / off-screen view) + * those are 0 and layout is wrong. Defer until the element is measurable. + */ + _scheduleLayoutWhenReady(callback){ + this._layoutScheduleToken = (this._layoutScheduleToken || 0) + 1 + const token = this._layoutScheduleToken + this._disconnectLayoutObserver() + + const attempt = () => { + if(token !== this._layoutScheduleToken) return(true) + if(!this._canRunAutoPlace()) return(false) + this._disconnectLayoutObserver() + callback() + return(true) + } + + if(attempt()) return + + this._layoutObserver = new ResizeObserver(() => { attempt() }) + this._layoutObserver.observe(this) + + let frames = 0 + const rafPoll = () => { + if(token !== this._layoutScheduleToken) return + if(attempt()) return + if(++frames < 12) requestAnimationFrame(rafPoll) + } + requestAnimationFrame(rafPoll) + } + + _scheduleAutoPlaceWhenReady(orientation, gapx, gapy, onDone){ + this._scheduleLayoutWhenReady(() => { + this.autoPlace(orientation, gapx, gapy) + if(typeof onDone === 'function') onDone() + }) } // Convert viewport (client) coordinates to this instance's SVG local coordinates. From d073a2e80414cc857c463de49bbdb603e528a692 Mon Sep 17 00:00:00 2001 From: STEINNI Date: Thu, 4 Jun 2026 11:22:39 +0000 Subject: [PATCH 36/36] better layout complete events --- bzGraflow.js | 111 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 81 insertions(+), 30 deletions(-) diff --git a/bzGraflow.js b/bzGraflow.js index 02fb486..352a92e 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -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)`