Compare commits

...

29 Commits

Author SHA1 Message Date
STEINNI 0c21f7b772 Graflow: unique refs for arrows to fix arrows in subflows 2026-05-05 13:26:29 +00:00
STEINNI 53ed4eea07 Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-04-21 13:58:50 +00:00
STEINNI 605398505a graflow: disabled attribute 2026-04-21 13:55:28 +00:00
STEINNI 4f728a3514 Graflow: standardized event signatures between floLoaded and subflowLoaded and subflowExited 2026-04-16 09:15:11 +00:00
STEINNI 3e7a82edc2 Graflow: standardized event signatures between floLoaded and subflowLoaded 2026-04-16 08:03:32 +00:00
STEINNI a871288c1c Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-04-14 14:10:35 +00:00
STEINNI 5f8e3865e3 Graflow: autofit fixes for subflows 2026-04-14 14:10:15 +00:00
STEINNI d25a4b548a Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-04-14 11:04:46 +00:00
STEINNI f031142848 Graflow: more flexible markup in ref-nodes 2026-04-14 11:04:24 +00:00
STEINNI b9b39a2679 Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-04-14 10:40:24 +00:00
STEINNI eb891c8cef Graflow: added parentNodeId to subflowLoaded event 2026-04-14 10:40:08 +00:00
STEINNI 974501fea8 Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-04-14 07:08:04 +00:00
STEINNI 40b12f0c47 Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-04-13 16:05:27 +00:00
STEINNI 33ea2bd672 Graflow: dataset.subflow 2026-04-13 16:05:12 +00:00
STEINNI ff4a25c1b2 Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-04-13 15:23:33 +00:00
STEINNI cfe33b8111 Graflow: autofit readded translations 2026-04-13 15:23:15 +00:00
STEINNI 7d3548d597 Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-04-13 15:21:52 +00:00
STEINNI 9381d82ae5 Graflow: autofit removed translations 2026-04-13 15:21:39 +00:00
STEINNI 839634a3ee Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-04-13 15:14:15 +00:00
STEINNI 0e6d23c1e1 Graflow: fixed autofit 2026-04-13 15:14:00 +00:00
STEINNI 8fc680fcb9 Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-04-13 14:34:19 +00:00
STEINNI 54eb584fd7 Graflow: autofit fix for subflows to include refNodes 2026-04-13 14:33:57 +00:00
STEINNI c16f4a1b42 Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-04-13 14:15:22 +00:00
STEINNI 0adc966608 Graflow: autofit as atrtibute 2026-04-13 14:14:59 +00:00
STEINNI 733420c90d Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-04-13 08:16:13 +00:00
STEINNI e4c4bb7f86 Graflow: subflow enter/exit tween aligned with tween param 2026-04-13 08:15:59 +00:00
STEINNI 2159c3881f Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-04-13 07:46:04 +00:00
STEINNI e1e5e8afce Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-04-10 16:40:55 +00:00
STEINNI 6641575423 Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-04-10 15:19:42 +00:00
9 changed files with 101 additions and 25 deletions
+1 -1
View File
@@ -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 ! */ bz-graflow .bzgf-nodes-container > * { /* allow the nodes to be moved ! */
pointer-events: auto; pointer-events: auto;
} }
bz-graflow .bzgf-nodes-container .bzgf-node{ position:absolute; } bz-graflow .bzgf-nodes-container .bzgf-node{ position:absolute; }
bz-graflow .bzgf-nodes-container .bzgf-fake-node{ bz-graflow .bzgf-nodes-container .bzgf-fake-node{
+92 -16
View File
@@ -30,6 +30,7 @@ class BZgraflow extends Buildoz{
this.stagedNodes = { } this.stagedNodes = { }
this.stagedWires = { } this.stagedWires = { }
this.arrowDefs = null this.arrowDefs = null
this.arrowMarkerId = `arrow-${crypto.randomUUID()}`
this.currentOrientation = null this.currentOrientation = null
} }
@@ -98,6 +99,28 @@ class BZgraflow extends Buildoz{
else this.initFlow() 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){ error(msg, err){
this.querySelector('.graflow-error')?.remove() this.querySelector('.graflow-error')?.remove()
const errorEl = document.createElement('div') const errorEl = document.createElement('div')
@@ -142,7 +165,10 @@ class BZgraflow extends Buildoz{
await this.loadNodes(flowObj.nodesFile) await this.loadNodes(flowObj.nodesFile)
this.flow = flowObj.flow this.flow = flowObj.flow
this.refresh() this.refresh()
this.fireEvent('flowLoaded', { url: source instanceof Blob ? null : source, blob: source instanceof Blob ? source : null }) this.fireEvent('flowLoaded', {
parentNodeId: null,
component: this,
})
} }
initFlow(){ initFlow(){
@@ -159,6 +185,8 @@ class BZgraflow extends Buildoz{
for(const tpl of doc.querySelectorAll('template')){ for(const tpl of doc.querySelectorAll('template')){
if(tpl.id=='svg-arrows'){ if(tpl.id=='svg-arrows'){
this.arrowDefs = tpl.querySelector('defs').cloneNode(true) this.arrowDefs = tpl.querySelector('defs').cloneNode(true)
const defaultArrow = this.arrowDefs.querySelector('#arrow')
if(defaultArrow) defaultArrow.id = this.arrowMarkerId
this.wiresContainer.appendChild(this.arrowDefs) this.wiresContainer.appendChild(this.arrowDefs)
} else { } else {
const rootEl = tpl.content.querySelector('.bzgf-node') const rootEl = tpl.content.querySelector('.bzgf-node')
@@ -214,6 +242,7 @@ class BZgraflow extends Buildoz{
this.enterSubflow(id) this.enterSubflow(id)
}) })
this.stagedNodes[id].appendChild(btnEnterSubflow) this.stagedNodes[id].appendChild(btnEnterSubflow)
this.stagedNodes[id].dataset.subflow = true
} }
this.nodesContainer.append(this.stagedNodes[id]) this.nodesContainer.append(this.stagedNodes[id])
if(!this.flow.nodes.find(n => n.id === id)) { if(!this.flow.nodes.find(n => n.id === id)) {
@@ -223,6 +252,7 @@ class BZgraflow extends Buildoz{
} }
enterSubflow(id){ enterSubflow(id){
if(this.disabled || this.hasAttribute('disabled')) return
const nodeEl = this.stagedNodes[id] const nodeEl = this.stagedNodes[id]
if(!nodeEl) return if(!nodeEl) return
@@ -235,7 +265,7 @@ class BZgraflow extends Buildoz{
const childEl = document.createElement('bz-graflow') const childEl = document.createElement('bz-graflow')
childEl.isSubflow = true childEl.isSubflow = true
childEl.currentOrientation = this.currentOrientation 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){ for(const attrName of inheritedAttrs){
if(this.hasAttribute(attrName)) childEl.setAttribute(attrName, this.getAttribute(attrName)) if(this.hasAttribute(attrName)) childEl.setAttribute(attrName, this.getAttribute(attrName))
} }
@@ -246,7 +276,7 @@ class BZgraflow extends Buildoz{
childEl.addNode({ childEl.addNode({
"nodeType": portLink.refNodeType, "nodeType": portLink.refNodeType,
"id": nid, "id": nid,
"markup": { "parentport": portLink.parentPort } "markup": { ...portLink }
}) })
if(portLink.direction=='in') { if(portLink.direction=='in') {
childEl.addWire({ childEl.addWire({
@@ -260,8 +290,8 @@ class BZgraflow extends Buildoz{
}) })
} }
} }
// Rebuild once refNodes are injected so the final refresh/autofit includes them.
childEl.autoPlace() childEl.refresh()
}, { once:true }) }, { once:true })
if(flowNode.subflow.url) childEl.setAttribute('flow', flowNode.subflow.url) if(flowNode.subflow.url) childEl.setAttribute('flow', flowNode.subflow.url)
@@ -271,7 +301,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. // Remember which node we "came from" so exitSubflow() can animate back to it.
childEl.dataset.enterNodeId = id childEl.dataset.enterNodeId = id
const btnExitSubflow = document.createElement('button') const btnExitSubflow = document.createElement('button')
@@ -308,7 +337,7 @@ class BZgraflow extends Buildoz{
// Force style flush, then animate back to identity (full parent size) // Force style flush, then animate back to identity (full parent size)
childEl.getBoundingClientRect() childEl.getBoundingClientRect()
childEl.style.transition = 'transform 1000ms ease-in-out' childEl.style.transition = `transform ${parseInt(this.getBZAttribute('tween')) || 500}ms ease-in-out`
requestAnimationFrame(() => { requestAnimationFrame(() => {
childEl.style.top = 0; childEl.style.top = 0;
childEl.style.left = 0; childEl.style.left = 0;
@@ -325,7 +354,10 @@ class BZgraflow extends Buildoz{
childEl.style.transform = 'none' // Important for nested subflows to position correctly childEl.style.transform = 'none' // Important for nested subflows to position correctly
childEl.style.willChange = '' childEl.style.willChange = ''
childEl.style.overflow = 'auto' childEl.style.overflow = 'auto'
this.fireEvent('subflowLoaded', { subflow: childEl }) this.fireEvent('subflowLoaded', {
parentNodeId: id,
component: childEl
})
}, { once:true }) }, { once:true })
} }
@@ -400,7 +432,9 @@ class BZgraflow extends Buildoz{
this.hostContainer.style.opacity = '1' this.hostContainer.style.opacity = '1'
this.hostContainer.style.visibility = 'visible' this.hostContainer.style.visibility = 'visible'
childEl.style.willChange = '' childEl.style.willChange = ''
this.fireEvent('subflowExited', { subflow: childEl }) this.fireEvent('subflowExited', {
component: this
})
}, { once:true }) }, { once:true })
} }
@@ -432,8 +466,8 @@ class BZgraflow extends Buildoz{
this.stagedWires[id] = document.createElementNS('http://www.w3.org/2000/svg', 'path') this.stagedWires[id] = document.createElementNS('http://www.w3.org/2000/svg', 'path')
this.stagedWires[id].setAttribute('d', path) this.stagedWires[id].setAttribute('d', path)
this.stagedWires[id].setAttribute('fill', 'none') this.stagedWires[id].setAttribute('fill', 'none')
if(this.arrowDefs && link.endArrow) this.stagedWires[id].setAttribute('marker-end','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(#arrow)') if(this.arrowDefs && link.startArrow) this.stagedWires[id].setAttribute('marker-start',`url(#${this.arrowMarkerId})`)
this.stagedWires[id].classList.add('bzgf-wire') this.stagedWires[id].classList.add('bzgf-wire')
this.stagedWires[id].dataset.id = id this.stagedWires[id].dataset.id = id
this.stagedWires[id].link = link this.stagedWires[id].link = link
@@ -473,6 +507,13 @@ class BZgraflow extends Buildoz{
} }
if(forceAutoplace) this.autoPlace(this.currentOrientation, parseInt(this.getBZAttribute('gapx')) || 80, parseInt(this.getBZAttribute('gapy')) || 80) if(forceAutoplace) this.autoPlace(this.currentOrientation, parseInt(this.getBZAttribute('gapx')) || 80, parseInt(this.getBZAttribute('gapy')) || 80)
this.fireEvent('refreshed', { }) 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. // Convert viewport (client) coordinates to this instance's SVG local coordinates.
@@ -1188,15 +1229,50 @@ class BZgraflow extends Buildoz{
} }
autofit(percent=100){ 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() const parentBB = this.parentElement.getBoundingClientRect()
// Use scroll dimensions for actual content extent (nodes can extend beyond element bounds) const gapx = parseInt(this.getBZAttribute('gapx')) || 80
const contentW = Math.max(this.scrollWidth || this.offsetWidth || 1, 1) const gapy = parseInt(this.getBZAttribute('gapy')) || 80
const contentH = Math.max(this.scrollHeight || this.offsetHeight || 1, 1) 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 sx = parentBB.width / contentW
const sy = parentBB.height / contentH const sy = parentBB.height / contentH
const scale = Math.min(sx, sy)*(percent/100) // uniform scale to fit inside parent const scale = Math.min(sx, sy)*(percent/100) // uniform scale to fit inside parent
this.style.transformOrigin = 'top left' const tx = Number.isFinite(left) ? (-left + gapx) : gapx
this.style.transform = `scale(${scale})` const ty = Number.isFinite(top) ? (-top + gapy) : gapy
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) Buildoz.define('graflow', BZgraflow)
+1 -1
View File
@@ -54,7 +54,7 @@
window.addEventListener('load',()=>{ window.addEventListener('load',()=>{
let grflw1 = document.querySelector('bz-graflow.compunet') let grflw1 = document.querySelector('bz-graflow.compunet')
grflw1.addEventListener('bz:graflow:subflowLoaded', grflw1.addEventListener('bz:graflow:subflowLoaded',
(evt) => { grflw1 = evt.detail.subflow } (evt) => { grflw1 = evt.detail.component }
) )
grflw1.addEventListener('bz:graflow:subflowExited', grflw1.addEventListener('bz:graflow:subflowExited',
(evt) => { grflw1 = evt.target } (evt) => { grflw1 = evt.target }
+1 -1
View File
@@ -54,7 +54,7 @@
window.addEventListener('load',()=>{ window.addEventListener('load',()=>{
let grflw3 = document.querySelector('bz-graflow.organi') let grflw3 = document.querySelector('bz-graflow.organi')
grflw3.addEventListener('bz:graflow:subflowLoaded', grflw3.addEventListener('bz:graflow:subflowLoaded',
(evt) => { grflw3 = evt.detail.subflow } (evt) => { grflw3 = evt.detail.component }
) )
grflw3.addEventListener('bz:graflow:subflowExited', grflw3.addEventListener('bz:graflow:subflowExited',
(evt) => { grflw3 = evt.target } (evt) => { grflw3 = evt.target }
+1 -1
View File
@@ -57,7 +57,7 @@
grflw2.setAttribute('align', document.querySelector('select[name="align"]').value) grflw2.setAttribute('align', document.querySelector('select[name="align"]').value)
grflw2.setAttribute('wiretype', document.querySelector('select[name="wiretype"]').value) grflw2.setAttribute('wiretype', document.querySelector('select[name="wiretype"]').value)
grflw2.addEventListener('bz:graflow:subflowLoaded', grflw2.addEventListener('bz:graflow:subflowLoaded',
(evt) => { grflw2 = evt.detail.subflow } (evt) => { grflw2 = evt.detail.component }
) )
grflw2.addEventListener('bz:graflow:subflowExited', grflw2.addEventListener('bz:graflow:subflowExited',
(evt) => { grflw2 = evt.target } (evt) => { grflw2 = evt.target }
+1 -1
View File
@@ -57,7 +57,7 @@
grflw4.setAttribute('align', document.querySelector('select[name="align"]').value) grflw4.setAttribute('align', document.querySelector('select[name="align"]').value)
grflw4.setAttribute('wiretype', document.querySelector('select[name="wiretype"]').value) grflw4.setAttribute('wiretype', document.querySelector('select[name="wiretype"]').value)
grflw4.addEventListener('bz:graflow:subflowLoaded', grflw4.addEventListener('bz:graflow:subflowLoaded',
(evt) => { grflw4 = evt.detail.subflow } (evt) => { grflw4 = evt.detail.component }
) )
grflw4.addEventListener('bz:graflow:subflowExited', grflw4.addEventListener('bz:graflow:subflowExited',
(evt) => { grflw4 = evt.target } (evt) => { grflw4 = evt.target }
+2 -2
View File
@@ -83,7 +83,7 @@
let grflw4 = document.querySelector('bz-graflow.icmp') let grflw4 = document.querySelector('bz-graflow.icmp')
grflw4.addEventListener('bz:graflow:subflowLoaded', grflw4.addEventListener('bz:graflow:subflowLoaded',
(evt) => { grflw4 = evt.detail.subflow } (evt) => { grflw4 = evt.detail.component }
) )
grflw4.addEventListener('bz:graflow:subflowExited', grflw4.addEventListener('bz:graflow:subflowExited',
(evt) => { grflw4 = evt.target } (evt) => { grflw4 = evt.target }
@@ -110,7 +110,7 @@
ro.observe(el) ro.observe(el)
let aifmi = null; let sidx=0; let aifmi = null; let sidx=0;
const sevanimation = () => { console.log('sevanimation') const sevanimation = () => {
severities.forEach(severity => aifmi.removeAttribute(severity)) severities.forEach(severity => aifmi.removeAttribute(severity))
aifmi.setAttribute(severities[sidx], '') aifmi.setAttribute(severities[sidx], '')
sidx++ sidx++
+1 -1
View File
@@ -54,7 +54,7 @@
window.addEventListener('load',()=>{ window.addEventListener('load',()=>{
let grflw1 = document.querySelector('bz-graflow.compunet') let grflw1 = document.querySelector('bz-graflow.compunet')
grflw1.addEventListener('bz:graflow:subflowLoaded', grflw1.addEventListener('bz:graflow:subflowLoaded',
(evt) => { grflw1 = evt.detail.subflow } (evt) => { grflw1 = evt.detail.component }
) )
grflw1.addEventListener('bz:graflow:subflowExited', grflw1.addEventListener('bz:graflow:subflowExited',
(evt) => { grflw1 = evt.target } (evt) => { grflw1 = evt.target }
+1 -1
View File
@@ -57,7 +57,7 @@
grflw1.setAttribute('align', document.querySelector('select[name="align"]').value) grflw1.setAttribute('align', document.querySelector('select[name="align"]').value)
grflw1.setAttribute('wiretype', document.querySelector('select[name="wiretype"]').value) grflw1.setAttribute('wiretype', document.querySelector('select[name="wiretype"]').value)
grflw1.addEventListener('bz:graflow:subflowLoaded', grflw1.addEventListener('bz:graflow:subflowLoaded',
(evt) => { grflw1 = evt.detail.subflow } (evt) => { grflw1 = evt.detail.component }
) )
grflw1.addEventListener('bz:graflow:subflowExited', grflw1.addEventListener('bz:graflow:subflowExited',
(evt) => { grflw1 = evt.target } (evt) => { grflw1 = evt.target }