Compare commits

...

49 Commits

Author SHA1 Message Date
STEINNI 9690401dad Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-06-05 15:16:04 +00:00
STEINNI 63f894f526 portal trick on bz-select options 2026-06-05 15:16:01 +00:00
STEINNI 345be1aa23 Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-06-04 11:27:24 +00:00
STEINNI e14b0c0214 Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-06-04 11:26:51 +00:00
STEINNI d073a2e804 better layout complete events 2026-06-04 11:22:39 +00:00
STEINNI 8f41a39a6e Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-05-22 09:17:50 +00:00
STEINNI df6ba88040 Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-05-22 09:16:32 +00:00
STEINNI 0b34b37aaf Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-05-22 09:15:42 +00:00
STEINNI 694e610ebb fixed bad autoplace if unfocused while autoplacing 2026-05-22 09:13:52 +00:00
STEINNI 3503fd90ab Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-05-05 13:26:49 +00:00
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 bf837e187b graflow: cleanup in subflow child params inheritance 2026-04-13 07:45:53 +00:00
STEINNI 88074b9144 graflow: cleanup in subflow child params inheritance 2026-04-13 07:45:31 +00:00
STEINNI e1e5e8afce Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-04-10 16:40:55 +00:00
STEINNI 7090bf7a3a graflow: fixed the underscore as separator in internal link storage 2026-04-10 16:40:31 +00:00
STEINNI 6641575423 Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-04-10 15:19:42 +00:00
STEINNI 93ea54da57 graflow: adjust loadnodes from object with clone to avoid polution 2026-04-10 15:19:21 +00:00
STEINNI 5906304b43 Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-04-10 14:51:02 +00:00
STEINNI 7f4e13c5e0 Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-03-11 22:07:45 +00:00
STEINNI f39f27efee Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-03-10 21:18:42 +00:00
STEINNI bd43063230 Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-03-10 21:17:25 +00:00
STEINNI 7d10993b66 Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-03-10 21:15:02 +00:00
STEINNI fd270d27da Merge branch 'main' of https://gitea.internike.com/nike/buildoz 2026-03-10 21:09:25 +00:00
10 changed files with 377 additions and 62 deletions
+16 -4
View File
@@ -72,7 +72,17 @@ bz-select > div.options-container{
transition: max-height 0.4s ease; transition: max-height 0.4s ease;
} }
bz-select > div.options-container.open{ pointer-events: auto; max-height: 10em;} bz-select > div.options-container.open{ pointer-events: auto; max-height: 10em;}
bz-select option{ div.options-container.portaled{
pointer-events: none;
position: fixed;
z-index: 10000;
max-height: 0;
overflow: auto;
transition: max-height 0.4s ease;
}
div.options-container.portaled.open{ pointer-events: auto; max-height: 10em;}
bz-select option,
div.options-container option{
background-color: #DDD; background-color: #DDD;
border: 1px solid black; border: 1px solid black;
color: #000; color: #000;
@@ -88,12 +98,14 @@ bz-select option{
margin-top 0.3s ease, margin-top 0.3s ease,
opacity 0.3s ease; opacity 0.3s ease;
} }
bz-select option.open{ bz-select option.open,
div.options-container option.open{
margin: 0; margin: 0;
opacity: 1; opacity: 1;
pointer-events: auto; pointer-events: auto;
} }
bz-select option:hover{ bz-select option:hover,
div.options-container option:hover{
background-color: #44F; background-color: #44F;
color: #FFF; color: #FFF;
} }
@@ -254,7 +266,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{
+107 -13
View File
@@ -135,7 +135,7 @@ class BZselect extends Buildoz {
if(!this.optionscontainer) this.optionscontainer = document.createElement('div') if(!this.optionscontainer) this.optionscontainer = document.createElement('div')
this.optionscontainer.classList.add('options-container') this.optionscontainer.classList.add('options-container')
this.append(this.optionscontainer) this.append(this.optionscontainer)
this.options = this.querySelectorAll('option') this.syncOptions()
if(this.#fillFromMarkup){ //can only do it once and only if fillOptions was not already called !! if(this.#fillFromMarkup){ //can only do it once and only if fillOptions was not already called !!
for(const opt of this.options){ for(const opt of this.options){
this.optionscontainer.append(opt) // Will move is to the right parent this.optionscontainer.append(opt) // Will move is to the right parent
@@ -147,6 +147,15 @@ class BZselect extends Buildoz {
} }
disconnectedCallback() {
if(this.open || this._closing) this.closeDropdown(true)
}
syncOptions() {
if(this.optionscontainer) this.options = this.optionscontainer.querySelectorAll('option')
else this.options = []
}
// static get observedAttributes(){ // Only if you want actions on attr change // static get observedAttributes(){ // Only if you want actions on attr change
// return([...super.observedAttributes, 'disabled']) // return([...super.observedAttributes, 'disabled'])
// } // }
@@ -172,19 +181,104 @@ class BZselect extends Buildoz {
} }
toggle(){ toggle(){
for(const opt of this.options){ if(this.open) this.closeDropdown()
if(this.open) { else this.openDropdown()
opt.classList.remove('open') }
this.optionscontainer.classList.remove('open')
} else { openDropdown() {
document.querySelectorAll('bz-select').forEach((sel) => { document.querySelectorAll('bz-select').forEach((sel) => {
if((sel!==this) && sel.open) sel.toggle() if((sel!==this) && sel.open) sel.closeDropdown(true)
}) })
opt.classList.add('open') this.optionscontainer.classList.add('portaled')
document.body.appendChild(this.optionscontainer)
this.positionPortal()
this.bindPortalListeners()
this.open = true
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if(!this.open) return
this.optionscontainer.classList.add('open') this.optionscontainer.classList.add('open')
for(const opt of this.options) opt.classList.add('open')
})
})
}
closeDropdown(immediate = false) {
if(!this.open) return
this.unbindPortalListeners()
for(const opt of this.options) opt.classList.remove('open')
this.optionscontainer.classList.remove('open')
if(immediate) {
this.clearCloseTransition()
this.finishCloseDropdown()
return
}
this._closing = true
this._onTransitionEnd = (evt) => {
if(evt.target !== this.optionscontainer) return
if(evt.propertyName !== 'max-height') return
this.finishCloseDropdownAfterTransition()
}
this.optionscontainer.addEventListener('transitionend', this._onTransitionEnd)
this._closeTimer = setTimeout(() => this.finishCloseDropdownAfterTransition(), 450)
}
finishCloseDropdownAfterTransition() {
if(!this._closing) return
this.clearCloseTransition()
this.finishCloseDropdown()
}
clearCloseTransition() {
this._closing = false
if(this._closeTimer) clearTimeout(this._closeTimer)
this._closeTimer = null
if(this._onTransitionEnd) {
this.optionscontainer.removeEventListener('transitionend', this._onTransitionEnd)
this._onTransitionEnd = null
} }
} }
this.open = !this.open
finishCloseDropdown() {
this.optionscontainer.classList.remove('portaled')
this.clearPortalStyles()
if(this.optionscontainer.parentElement !== this) this.append(this.optionscontainer)
this.open = false
}
positionPortal() {
const rect = this.button.getBoundingClientRect()
this.optionscontainer.style.top = `${rect.bottom}px`
this.optionscontainer.style.left = `${rect.left}px`
this.optionscontainer.style.width = `${rect.width}px`
}
clearPortalStyles() {
this.optionscontainer.style.top = ''
this.optionscontainer.style.left = ''
this.optionscontainer.style.width = ''
}
bindPortalListeners() {
this._repositionBound = this.positionPortal.bind(this)
window.addEventListener('scroll', this._repositionBound, true)
window.addEventListener('resize', this._repositionBound)
this._outsideClickBound = (evt) => {
if(this.open && !this.contains(evt.target) && !this.optionscontainer.contains(evt.target)) {
this.closeDropdown()
}
}
this._outsideClickTimer = setTimeout(() => {
document.addEventListener('click', this._outsideClickBound, true)
}, 0)
}
unbindPortalListeners() {
if(this._repositionBound) window.removeEventListener('scroll', this._repositionBound, true)
if(this._repositionBound) window.removeEventListener('resize', this._repositionBound)
if(this._outsideClickBound) document.removeEventListener('click', this._outsideClickBound, true)
if(this._outsideClickTimer) clearTimeout(this._outsideClickTimer)
this._outsideClickTimer = null
} }
onClick(evt){ onClick(evt){
@@ -196,7 +290,7 @@ class BZselect extends Buildoz {
onOption(value, silent=false){ onOption(value, silent=false){
if(this.getAttribute('disabled') !== null) return if(this.getAttribute('disabled') !== null) return
this.value = value this.value = value
if(!silent) this.toggle() if(!silent && this.open) this.closeDropdown()
} }
addOption(value, markup){ addOption(value, markup){
@@ -207,16 +301,16 @@ class BZselect extends Buildoz {
opt.addEventListener('click',this.onClick.bind(this)) opt.addEventListener('click',this.onClick.bind(this))
if(!this.optionscontainer) this.optionscontainer = document.createElement('div') if(!this.optionscontainer) this.optionscontainer = document.createElement('div')
this.optionscontainer.append(opt) this.optionscontainer.append(opt)
this.options = this.querySelectorAll('option') this.syncOptions()
this.#fillFromMarkup = false this.#fillFromMarkup = false
} }
fillOptions(opts, erase = true){ fillOptions(opts, erase = true){
// Caution: you cannot count on connectedCallback to have run already, because one might fill before adding to the DOM // Caution: you cannot count on connectedCallback to have run already, because one might fill before adding to the DOM
if(erase){ if(erase){
this.options = this.querySelectorAll('option') this.syncOptions()
this.options.forEach(node => { node.remove() }) this.options.forEach(node => { node.remove() })
this.options = this.querySelectorAll('option') this.syncOptions()
this.onOption('', true) // unselect last this.onOption('', true) // unselect last
} }
for(const opt of opts) this.addOption(opt.value, opt.markup) for(const opt of opts) this.addOption(opt.value, opt.markup)
+242 -33
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')
@@ -132,7 +155,7 @@ class BZgraflow extends Buildoz{
return return
} }
} else if(typeof source == 'object') { } else if(typeof source == 'object') {
flowObj = source flowObj = structuredClone(source)
} }
if(!flowObj.nodesFile){ if(!flowObj.nodesFile){
@@ -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,6 +265,10 @@ 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', 'autofit'] // ! Not 'isolated' !
for(const attrName of inheritedAttrs){
if(this.hasAttribute(attrName)) childEl.setAttribute(attrName, this.getAttribute(attrName))
}
childEl.addEventListener('bz:graflow:flowLoaded', (e) => { childEl.addEventListener('bz:graflow:flowLoaded', (e) => {
for(const portLink of flowNode.subflow.portLinks){ for(const portLink of flowNode.subflow.portLinks){
@@ -242,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({
@@ -256,8 +290,8 @@ class BZgraflow extends Buildoz{
}) })
} }
} }
// Rebuild once refNodes are injected so the final refresh/autofit includes them.
childEl.autoPlace(this.currentOrientation, parseInt(this.getBZAttribute('gapx')) || 80, parseInt(this.getBZAttribute('gapy')) || 80) 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)
@@ -267,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')
@@ -280,8 +313,6 @@ class BZgraflow extends Buildoz{
this.invade(this, childEl) this.invade(this, childEl)
childEl.hostContainer.appendChild(btnExitSubflow) childEl.hostContainer.appendChild(btnExitSubflow)
/////////////////////
// Fade out the current (host) graflow while the child scales up // Fade out the current (host) graflow while the child scales up
this.hostContainer.style.opacity = '1' this.hostContainer.style.opacity = '1'
this.hostContainer.style.transition = 'opacity 1000ms ease-in-out' this.hostContainer.style.transition = 'opacity 1000ms ease-in-out'
@@ -306,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;
@@ -323,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 })
} }
@@ -398,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 })
} }
@@ -414,6 +450,10 @@ class BZgraflow extends Buildoz{
return(this.stagedNodes[nid]) return(this.stagedNodes[nid])
} }
makeWireId(nid1, nid2){
return(`${encodeURIComponent(nid1)}|${encodeURIComponent(nid2)}`)
}
addWire(link){ addWire(link){
const [idNode1, idPort1] = link.from const [idNode1, idPort1] = link.from
const [idNode2, idPort2] = link.to const [idNode2, idPort2] = link.to
@@ -422,12 +462,12 @@ class BZgraflow extends Buildoz{
return return
} }
const path = this.linkNodes(idNode1, idPort1, idNode2, idPort2) 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] = 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
@@ -465,8 +505,97 @@ class BZgraflow extends Buildoz{
else this.currentOrientation = 'vertical' else this.currentOrientation = 'vertical'
} }
} }
if(forceAutoplace) this.autoPlace(this.currentOrientation, parseInt(this.getBZAttribute('gapx')) || 80, parseInt(this.getBZAttribute('gapy')) || 80) const gapx = parseInt(this.getBZAttribute('gapx')) || 80
const gapy = parseInt(this.getBZAttribute('gapy')) || 80
const finishRefresh = () => {
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
if(this._canRunAutoPlace()) this.autofit(autofitPercent)
else this._scheduleLayoutWhenReady(() => this.autofit(autofitPercent))
}
}
const onLayoutComplete = () => {
this.fireEvent('layoutComplete', { })
finishRefresh()
}
if(forceAutoplace) this._scheduleAutoPlaceWhenReady(this.currentOrientation, gapx, gapy, onLayoutComplete)
else onLayoutComplete()
}
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, onLayoutComplete){
this._scheduleLayoutWhenReady(() => {
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. // Convert viewport (client) coordinates to this instance's SVG local coordinates.
@@ -656,7 +785,8 @@ class BZgraflow extends Buildoz{
return(path) return(path)
} }
autoPlace(orientation = 'horizontal', 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(gapx == null) gapx = parseInt(this.getBZAttribute('gapx')) || 80
if(gapy == null) gapy = parseInt(this.getBZAttribute('gapy')) || 80 if(gapy == null) gapy = parseInt(this.getBZAttribute('gapy')) || 80
if(tween == null) tween = parseInt(this.getBZAttribute('tween')) || 500 if(tween == null) tween = parseInt(this.getBZAttribute('tween')) || 500
@@ -667,6 +797,9 @@ class BZgraflow extends Buildoz{
// moveNode() checks this token each frame and will no-op if superseded. // moveNode() checks this token each frame and will no-op if superseded.
this._autoPlaceToken = (this._autoPlaceToken || 0) + 1 this._autoPlaceToken = (this._autoPlaceToken || 0) + 1
const token = this._autoPlaceToken const token = this._autoPlaceToken
this._layoutMovePending = 0
this._layoutCompleteToken = token
this._layoutCompleteHandler = (typeof onLayoutComplete === 'function') ? onLayoutComplete : null
// Cleanup placeholders from previous autoPlace() runs. // Cleanup placeholders from previous autoPlace() runs.
// Each run creates new longLinkPlaceHolder_* IDs; without cleanup they accumulate in the DOM. // Each run creates new longLinkPlaceHolder_* IDs; without cleanup they accumulate in the DOM.
@@ -790,7 +923,7 @@ class BZgraflow extends Buildoz{
y = Math.max(parentsY[parents[nid][0]], y) //TODO handle multiple parents with avg y = Math.max(parentsY[parents[nid][0]], y) //TODO handle multiple parents with avg
} }
placedY = y 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)) { if((align == 'parent') && (nid in parents) && (parents[nid][0] in parentsY)) {
parentsY[parents[nid][0]] += gapy + nodeHeight parentsY[parents[nid][0]] += gapy + nodeHeight
} else { } else {
@@ -803,7 +936,7 @@ class BZgraflow extends Buildoz{
} }
placedY = y placedY = y
this.addFakeNode(nid, x, y, wMax*0.75, fakeNodeHeight) this.addFakeNode(nid, x, y, wMax*0.75, fakeNodeHeight)
this.moveNode(nid, x, y, orientation, tween, 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 // Never increment parentsY for fake nodes: they're placeholders and must not disalign real children
y = Math.max(y, placedY + gapy + fakeNodeHeight) y = Math.max(y, placedY + gapy + fakeNodeHeight)
} }
@@ -824,7 +957,7 @@ class BZgraflow extends Buildoz{
!nid.startsWith('longLinkPlaceHolder_') && nid in parents && parents[nid][0] === pid !nid.startsWith('longLinkPlaceHolder_') && nid in parents && parents[nid][0] === pid
) )
if(firstRealChild && nodeY[pid] !== nodeY[firstRealChild]){ 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] nodeY[pid] = nodeY[firstRealChild]
} }
} }
@@ -853,17 +986,18 @@ class BZgraflow extends Buildoz{
for(const nid of layer){ for(const nid of layer){
if(!nid.startsWith('longLinkPlaceHolder_')){ if(!nid.startsWith('longLinkPlaceHolder_')){
const bb = this.stagedNodes[nid].getBoundingClientRect() const bb = this.stagedNodes[nid].getBoundingClientRect()
this.moveNode(nid, x, y, orientation, tween, null, token) this.moveNode(nid, x, y, orientation, tween, token)
x += gapx + (this.stagedNodes[nid].offsetWidth || bb.width) x += gapx + (this.stagedNodes[nid].offsetWidth || bb.width)
} else { } else {
this.addFakeNode(nid, x, y, fakeNodeWidth, hMax*0.75) this.addFakeNode(nid, x, y, fakeNodeWidth, hMax*0.75)
this.moveNode(nid, x, y, orientation, tween, null, token) this.moveNode(nid, x, y, orientation, tween, token)
x += gapx + fakeNodeWidth x += gapx + fakeNodeWidth
} }
} }
y += hMax + gapy y += hMax + gapy
} }
} }
this._maybeFireLayoutComplete()
} }
clearFakeNodes(){ clearFakeNodes(){
@@ -983,17 +1117,34 @@ class BZgraflow extends Buildoz{
} }
moveNode(nid, destx, desty, orientation, duration = 200, autoPlaceToken = null) { moveNode(nid, destx, desty, orientation, duration = 200, autoPlaceToken = null) {
const t0 = performance.now()
const el0 = this.stagedNodes?.[nid] const el0 = this.stagedNodes?.[nid]
if(!el0) return 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 bb = el0.getBoundingClientRect()
const parentbb = el0.parentElement.getBoundingClientRect() const parentbb = el0.parentElement.getBoundingClientRect()
const x0=bb.x - parentbb.x const x0=bb.x - parentbb.x
const y0 = bb.y - parentbb.y const y0 = bb.y - parentbb.y
function frame(t) { function frame(t) {
if(autoPlaceToken && autoPlaceToken !== this._autoPlaceToken) return if(autoPlaceToken && autoPlaceToken !== this._autoPlaceToken) {
finishLayoutMove()
return
}
const el = this.stagedNodes?.[nid] const el = this.stagedNodes?.[nid]
if(!el) return if(!el) {
finishLayoutMove()
return
}
const p = Math.min((t - t0) / duration, 1) const p = Math.min((t - t0) / duration, 1)
const k = p * p * (3 - 2 * p) // smoothstep const k = p * p * (3 - 2 * p) // smoothstep
const x = x0 + (destx - x0) * k const x = x0 + (destx - x0) * k
@@ -1011,6 +1162,7 @@ class BZgraflow extends Buildoz{
flowNode.coords.y = y flowNode.coords.y = y
} }
this.fireEvent('nodeMoved', { nid, x, y }) this.fireEvent('nodeMoved', { nid, x, y })
finishLayoutMove()
} }
} }
requestAnimationFrame(frame.bind(this)) requestAnimationFrame(frame.bind(this))
@@ -1018,10 +1170,14 @@ class BZgraflow extends Buildoz{
updateWires(nid, orientation, LondLinkfix = false){ updateWires(nid, orientation, LondLinkfix = false){
const wires = Object.keys(this.stagedWires) const wires = Object.keys(this.stagedWires)
.filter(id => (id.startsWith(nid+'_')||id.endsWith('_'+nid)))
.map(id => this.stagedWires[id]) .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){ 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) const lnk = this.getLink(nid1, nid2)
if(!lnk) continue if(!lnk) continue
if(!this.flow?.longLinks) this.flow.longLinks = [] if(!this.flow?.longLinks) this.flow.longLinks = []
@@ -1038,7 +1194,7 @@ class BZgraflow extends Buildoz{
} }
getLink(nid1, nid2){ getLink(nid1, nid2){
const wire = this.stagedWires[`${nid1}_${nid2}`] const wire = this.stagedWires[this.makeWireId(nid1, nid2)]
if(wire?.link) return wire.link if(wire?.link) return wire.link
return this._virtualLinks?.get(`${nid1}__${nid2}`) ?? null return this._virtualLinks?.get(`${nid1}__${nid2}`) ?? null
} }
@@ -1176,16 +1332,69 @@ class BZgraflow extends Buildoz{
return(crossLayerLinks) return(crossLayerLinks)
} }
/**
* 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
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 gapx = parseInt(this.getBZAttribute('gapx')) || 80
const gapy = parseInt(this.getBZAttribute('gapy')) || 80
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){ 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 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 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 = 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)`
} else {
this.style.transform = `scale(${scale})` 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)
@@ -1442,7 +1651,7 @@ class EditWires{
return('') return('')
} }
this.graflow.addWire({ from: [idNode1, idPort1], to: [idNode2, idPort2] }) 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){ onSelectWire(e){
@@ -1461,7 +1670,7 @@ class EditWires{
const wireId = this.currentlySelectedWire.dataset.id const wireId = this.currentlySelectedWire.dataset.id
const linkToRemove = this.graflow.stagedWires[wireId]?.link const linkToRemove = this.graflow.stagedWires[wireId]?.link
this.graflow.flow.links = this.graflow.flow.links.filter(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() this.graflow.stagedWires[wireId]?.remove()
delete this.graflow.stagedWires[wireId] delete this.graflow.stagedWires[wireId]
+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 }