diff --git a/buildoz.css b/buildoz.css index 0a556b4..8916695 100644 --- a/buildoz.css +++ b/buildoz.css @@ -204,3 +204,38 @@ bz-graflow .bzgf-main-container{ position: relative; 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 */ diff --git a/bzGraflow.js b/bzGraflow.js index b7f7176..249bffd 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -34,6 +34,32 @@ class BZgraflow extends Buildoz{ 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) { el.innerHTML = `` } @@ -41,10 +67,7 @@ class BZgraflow extends Buildoz{ connectedCallback() { super.connectedCallback() const flowUrl = this.getBZAttribute('flow') - if(!flowUrl) { - console.warn('BZgraflow: No flow URL !?') - return - } + if(!flowUrl) return // Be tolerant: maybe injected later from JS above // If attribute "isolated" is present, render inside a shadow root. // Otherwise, render in light DOM (no shadow DOM). this.hostContainer = document.createElement('div') @@ -52,37 +75,7 @@ class BZgraflow extends Buildoz{ this.mainContainer = this.hasAttribute('isolated') ? this.hostContainer.attachShadow({ mode: 'open' }) : this.hostContainer - const style = document.createElement('style') - //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.ensureIsolatedCoreStyles() this.nodesContainer = document.createElement('div') this.nodesContainer.classList.add('bzgf-nodes-container') this.wiresContainer = document.createElementNS('http://www.w3.org/2000/svg', 'svg') @@ -91,9 +84,13 @@ class BZgraflow extends Buildoz{ this.mainContainer.append(this.wiresContainer) this.mainContainer.append(this.nodesContainer) 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){ this.innerHTML = `
${msg}
` if(err) console.error(msg, err) @@ -290,11 +287,7 @@ class BZgraflow extends Buildoz{ childEl.addEventListener('transitionend', (e) => { if(e.propertyName !== 'transform') return this.hostContainer.style.visibility = 'hidden' - // Important for nested subflows: - // 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.transform = 'none' // Important for nested subflows to position correctly childEl.style.willChange = '' this.dispatchEvent(new CustomEvent('subflowLoaded', { detail: { subflow: childEl }, @@ -1031,7 +1024,69 @@ class BZgraflow extends Buildoz{ } return(crossLayerLinks) } -} +} 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 + } +} \ No newline at end of file