graflow: editable=>moving nodes

This commit is contained in:
STEINNI
2026-02-28 22:18:14 +00:00
parent e2c4ca8382
commit 9bc55b74d6
2 changed files with 133 additions and 43 deletions

View File

@@ -204,3 +204,38 @@ bz-graflow .bzgf-main-container{
position: relative; position: relative;
box-sizing: border-box; box-sizing: border-box;
} }
/* BZGRAFLOW_CORE_START */
/* bz-graflow internal layout rules (used in light DOM, and injected into shadow DOM when isolated) */
bz-graflow .bzgf-wires-container,
bz-graflow .bzgf-nodes-container{
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
bz-graflow .bzgf-nodes-container .bzgf-node{ position:absolute; }
bz-graflow .bzgf-nodes-container .bzgf-fake-node{
position: absolute;
width: 5px;
height: 5px;
background: transparent;
border-style: none;
}
bz-graflow .bzgf-nodes-container button.bzgf-zoom-in{
z-index: 999;
position: absolute;
top: -0.5em;
right: -1em;
color: black;
width: 2em;
height: 2em;
padding: 0;
}
bz-graflow button.bzgf-zoom-out{
z-index: 999;
position: absolute;
left: 5px;
top: 5px;
}
/* BZGRAFLOW_CORE_END */

View File

@@ -34,6 +34,32 @@ class BZgraflow extends Buildoz{
this.currentOrientation = null this.currentOrientation = null
} }
static _coreCssPromise = null
static async getCoreCss(){
if(BZgraflow._coreCssPromise) return(await BZgraflow._coreCssPromise)
BZgraflow._coreCssPromise = (async() => {
const res = await fetch('/app/thirdparty/buildoz/buildoz.css')
const css = await res.text()
const m = css.match(/\/\*\s*BZGRAFLOW_CORE_START\s*\*\/([\s\S]*?)\/\*\s*BZGRAFLOW_CORE_END\s*\*\//)
const core = m ? m[1] : ''
return(core)
})()
return(await BZgraflow._coreCssPromise)
}
async ensureIsolatedCoreStyles(){
if(!this.hasAttribute('isolated')) return
if(this._isolatedCoreInjected) return
this._isolatedCoreInjected = true
const core = await BZgraflow.getCoreCss()
// Convert light-dom selectors (`bz-graflow ...`) to shadow-dom selectors (`:host ...`)
const shadowCss = core.replaceAll('bz-graflow', ':host')
const style = document.createElement('style')
style.textContent = shadowCss
this.mainContainer.appendChild(style)
}
addIcon(el, name) { addIcon(el, name) {
el.innerHTML = `<svg viewBox="0 0 1024 1024" width="20" height="20" fill="#000000"><g transform="rotate(90 512 512)"><path d="${this.btnIcons[name]}"/></g></svg>` el.innerHTML = `<svg viewBox="0 0 1024 1024" width="20" height="20" fill="#000000"><g transform="rotate(90 512 512)"><path d="${this.btnIcons[name]}"/></g></svg>`
} }
@@ -41,10 +67,7 @@ class BZgraflow extends Buildoz{
connectedCallback() { connectedCallback() {
super.connectedCallback() super.connectedCallback()
const flowUrl = this.getBZAttribute('flow') const flowUrl = this.getBZAttribute('flow')
if(!flowUrl) { if(!flowUrl) return // Be tolerant: maybe injected later from JS above
console.warn('BZgraflow: No flow URL !?')
return
}
// If attribute "isolated" is present, render inside a shadow root. // If attribute "isolated" is present, render inside a shadow root.
// Otherwise, render in light DOM (no shadow DOM). // Otherwise, render in light DOM (no shadow DOM).
this.hostContainer = document.createElement('div') this.hostContainer = document.createElement('div')
@@ -52,37 +75,7 @@ class BZgraflow extends Buildoz{
this.mainContainer = this.hasAttribute('isolated') this.mainContainer = this.hasAttribute('isolated')
? this.hostContainer.attachShadow({ mode: 'open' }) ? this.hostContainer.attachShadow({ mode: 'open' })
: this.hostContainer : this.hostContainer
const style = document.createElement('style') this.ensureIsolatedCoreStyles()
//TODO kick this wart somewhere under a carpet
style.textContent = `
.bzgf-wires-container,
.bzgf-nodes-container{ position: absolute; inset: 0; width: 100%; height: 100%; }
.bzgf-nodes-container .bzgf-node{ position:absolute; }
.bzgf-nodes-container .bzgf-fake-node{
position: absolute;
width: 5px;
height: 5px;
backgrround: transparent;
border-style: none;
}
.bzgf-nodes-container button.bzgf-zoom-in{
z-index: 999;
position: absolute;
top: -0.5em;
right: -1em;
color: black;
width: 2em;
height: 2em;
padding: 0;
}
bz-graflow button.bzgf-zoom-out{
z-index: 999;
position: absolute;
left: 5px;
top: 5px;
}
`
this.mainContainer.appendChild(style)
this.nodesContainer = document.createElement('div') this.nodesContainer = document.createElement('div')
this.nodesContainer.classList.add('bzgf-nodes-container') this.nodesContainer.classList.add('bzgf-nodes-container')
this.wiresContainer = document.createElementNS('http://www.w3.org/2000/svg', 'svg') this.wiresContainer = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
@@ -91,7 +84,11 @@ class BZgraflow extends Buildoz{
this.mainContainer.append(this.wiresContainer) this.mainContainer.append(this.wiresContainer)
this.mainContainer.append(this.nodesContainer) this.mainContainer.append(this.nodesContainer)
this.append(this.hostContainer) this.append(this.hostContainer)
this.loadFlow(flowUrl) // Let it load async this.loadFlow(flowUrl).then(() => {
if(this.hasAttribute('editable')){
this.dnd = new MovingNodes(this)
this.dnd.enableMovingNodes('.bzgf-node') }
}) // Let it load async
} }
error(msg, err){ error(msg, err){
@@ -290,11 +287,7 @@ class BZgraflow extends Buildoz{
childEl.addEventListener('transitionend', (e) => { childEl.addEventListener('transitionend', (e) => {
if(e.propertyName !== 'transform') return if(e.propertyName !== 'transform') return
this.hostContainer.style.visibility = 'hidden' this.hostContainer.style.visibility = 'hidden'
// Important for nested subflows: childEl.style.transform = 'none' // Important for nested subflows to position correctly
// A non-'none' transform on this element creates a containing block, which would make
// any nested `position:fixed` subflow overlay position relative to this element instead
// of the viewport (showing up as an extra offset like 8px).
childEl.style.transform = 'none'
childEl.style.willChange = '' childEl.style.willChange = ''
this.dispatchEvent(new CustomEvent('subflowLoaded', { this.dispatchEvent(new CustomEvent('subflowLoaded', {
detail: { subflow: childEl }, detail: { subflow: childEl },
@@ -1031,7 +1024,69 @@ class BZgraflow extends Buildoz{
} }
return(crossLayerLinks) return(crossLayerLinks)
} }
}
}
Buildoz.define('graflow', BZgraflow) Buildoz.define('graflow', BZgraflow)
class MovingNodes{
constructor(graflow){
this.graflow = graflow
this.nodesContainer = this.graflow.mainContainer.querySelector('.bzgf-nodes-container')
this.state = null
}
enableMovingNodes(itemSelector, handleSelector = itemSelector) {
this.itemSelector = itemSelector
this.handleSelector = handleSelector
if(!this._handleCursorStyle){
const style = document.createElement('style')
style.textContent = `${handleSelector}{ cursor: move }`
this.nodesContainer.appendChild(style)
this._handleCursorStyle = style
}
this.nodesContainer.addEventListener('pointerdown', this.pointerDown.bind(this))
this.nodesContainer.addEventListener('pointermove', this.pointerMove.bind(this))
this.nodesContainer.addEventListener('pointerup', this.pointerUp.bind(this))
}
pointerDown(e){
const node = (e.target.classList.contains(this.itemSelector)) ? e.target : e.target.closest(this.itemSelector)
const handle = (node.classList.contains(this.handleSelector)) ? node : node.querySelector(this.handleSelector)
const rect = node.getBoundingClientRect()
this.state = {
node,
handle,
startX: e.clientX,
startY: e.clientY,
offsetX: rect.left,
offsetY: rect.top
}
const x = e.clientX - this.state.startX + this.state.offsetX
const y = e.clientY - this.state.startY + this.state.offsetY
node.setPointerCapture(e.pointerId)
node.style.position = 'absolute'
node.style.left = `${x}px`
node.style.top = `${y}px`
node.style.margin = '0'
node.style.zIndex = '9999'
}
pointerMove(e){
if(!this.state) return
const { node, startX, startY, offsetX, offsetY } = this.state
const x = e.clientX - startX + offsetX
const y = e.clientY - startY + offsetY
node.style.left = `${x}px`
node.style.top = `${y}px`
this.graflow.updateWires(node.dataset.id, this.graflow.currentOrientation)
}
pointerUp(e){
if(!this.state) return
this.state.node.releasePointerCapture(e.pointerId)
this.state = null
}
}