diff --git a/bzGraflow.js b/bzGraflow.js index eb698b4..cf60522 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -113,7 +113,7 @@ class BZgraflow extends Buildoz{ } // Now load styles (once) - if(!BZgraflow._loadedNodeStyles.has(url)) { + if(!BZgraflow._loadedNodeStyles.has(url) || this.attributes.isolated) { const styles = doc.querySelectorAll('style') styles.forEach(styleEl => { const style = document.createElement('style') @@ -151,39 +151,65 @@ class BZgraflow extends Buildoz{ } zoomIn(id){ - console.log('==============================>ZOOM IN:', id) - const node = this.stagedNodes[id] - const nodeBB = node.getBoundingClientRect() - const parentBB = this.nodesContainer.getBoundingClientRect() - const sx = parentBB.width / nodeBB.width - const sy = parentBB.height / nodeBB.height - const tx = parentBB.left - nodeBB.left + this.nodesContainer.scrollLeft // TODO Should have a meth to accumulate scrolls in ancestors - const ty = parentBB.top - nodeBB.top + this.nodesContainer.scrollTop // TODO Should have a meth to accumulate scrolls in ancestors - node.style.setProperty('--tx', tx + 'px') - node.style.setProperty('--ty', ty + 'px') - node.style.setProperty('--sx', sx) - node.style.setProperty('--sy', sy) - node.style.zIndex = '9999' - node.classList.add('scaler') - Promise.all(node.getAnimations().map(a => a.finished)).then((transitions) => { -const testEl = document.createElement('bz-graflow') -testEl.setAttribute('flow', '/app/assets/json/bzGraflow/testFlowEic.json') -testEl.setAttribute('tension', '60') -testEl.classList.add('eic') + const nodeEl = this.stagedNodes[id] + if(!nodeEl) return - this.Invade(this, testEl) - node.classList.remove('scaler') + // Create the child graflow first, place it above (foreground) the current graflow, + // 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 + + const childEl = document.createElement('bz-graflow') + childEl.setAttribute('flow', flowUrl) + childEl.setAttribute('tension', this.getBZAttribute('tension') || '60') + childEl.style.zIndex = '9999' + + // Put the child in the exact same viewport rect as the parent (fixed overlay) + this.Invade(this, childEl, { hideOld:false }) + + // Initial transform so the full-size child "fits" inside the node + const sx0 = nodeBB.width / parentBB.width + const sy0 = nodeBB.height / parentBB.height + const tx0 = nodeBB.left - parentBB.left + const ty0 = nodeBB.top - parentBB.top + + // Inline "scaler" (shadow styles don't apply to the child element) + childEl.style.transformOrigin = 'top left' + childEl.style.willChange = 'transform' + childEl.style.transition = 'transform 300ms ease-in-out' + childEl.style.transform = 'translate(var(--tx, 0px), var(--ty, 0px)) scale(var(--sx, 1), var(--sy, 1))' + childEl.style.setProperty('--tx', tx0 + 'px') + childEl.style.setProperty('--ty', ty0 + 'px') + childEl.style.setProperty('--sx', sx0) + childEl.style.setProperty('--sy', sy0) + + // Force style flush, then animate back to identity (full parent size) + childEl.getBoundingClientRect() + requestAnimationFrame(() => { + childEl.style.setProperty('--tx', '0px') + childEl.style.setProperty('--ty', '0px') + childEl.style.setProperty('--sx', 1) + childEl.style.setProperty('--sy', 1) }) + + childEl.addEventListener('transitionend', (e) => { + if(e.propertyName !== 'transform') return + this.style.visibility = 'hidden' + }, { once:true }) } - Invade(oldEl, newEl){ + Invade(oldEl, newEl, { hideOld=true } = {}){ const r = oldEl.getBoundingClientRect() - oldEl.style.visibility = 'hidden' + if(hideOld) oldEl.style.visibility = 'hidden' newEl.style.position = 'fixed' newEl.style.left = r.left + 'px' newEl.style.top = r.top + 'px' newEl.style.width = r.width + 'px' newEl.style.height = r.height + 'px' + newEl.style.display = 'block' oldEl.parentNode.appendChild(newEl) } @@ -389,21 +415,40 @@ testEl.classList.add('eic') layerWidths.push(totWidth) } + // Temporary "virtual" links used only during autoPlace() to let reorderLayers() + // reason about placeholder nodes as if they were part of the original long-link. + // This prevents bogus swaps caused by missing port info on placeholders. + this._virtualLinks = new Map() + // If any long-links, create placeholders for skipped layers this.flow.longLinks = this.findLongLinks(this.flow.links) - for(const link of this.flow.longLinks){ - for(const layerIdx of link.skippedLayers){ + for(const llink of this.flow.longLinks){ + let fakeParent = llink.link.from[0] + for(const layerIdx of llink.skippedLayers){ const nid = `longLinkPlaceHolder_${crypto.randomUUID()}` layers[layerIdx].push(nid) - link.interNodes.push(nid) + llink.interNodes.push(nid) + // Placeholders are added after initial index computation; give them an index + // so reorderLayers() can take them into account (otherwise they default to base=0). + indexes[nid] = { base: layers[layerIdx].length - 1, ports: {} } + // Virtual link: treat placeholder as receiving the same "from port" as the original long-link. + // (Child port doesn't matter for placeholders since they have no ports.) + this._virtualLinks.set(`${fakeParent}__${nid}`, { + from: [fakeParent, llink.link.from[1]], + to: [nid, llink.link.to[1]], + }) + parents[nid] = [fakeParent] + fakeParent = nid } } // Reorder layers to avoid crossings thanks to indexes this.reorderLayers(layers, parents, indexes, orientation) + delete this._virtualLinks // Finally place everything if(orientation=='horizontal'){ + const fakeNodeHeight = 10 let x = gapx for(const [idx, layer] of layers.entries()){ let wMax = this.getMaxWidth(layer) @@ -414,14 +459,15 @@ testEl.classList.add('eic') this.moveNode(nid, x, y, orientation, tween) y += gapy + bb.height } else { - this.addFakeNode(nid, x, y, wMax*0.75, 10) + this.addFakeNode(nid, x, y, wMax*0.75, fakeNodeHeight) this.moveNode(nid, x, y, orientation, tween) - y += gapy + 10 //TODO + y += gapy + fakeNodeHeight } } x += wMax + gapx } } else if(orientation=='vertical'){ + const fakeNodeWidth = 10 let y = gapy for(const [idx, layer] of layers.entries()){ let hMax = this.getMaxHeight(layer) @@ -432,9 +478,9 @@ testEl.classList.add('eic') this.moveNode(nid, x, y, orientation, tween) x += gapx + bb.width } else { - this.addFakeNode(nid, x, y, 10, hMax*0.75) + this.addFakeNode(nid, x, y, fakeNodeWidth, hMax*0.75) this.moveNode(nid, x, y, orientation, tween) - x += gapx + 10 //TODO + x += gapx + fakeNodeWidth } } y += hMax + gapy @@ -461,12 +507,11 @@ testEl.classList.add('eic') computePortOffsets(nid, orientation = 'horizontal'){ const node = this.stagedNodes[nid] if(!node || !node.ports) return({}) - const axis = (orientation === 'vertical') ? 'x' : 'y' const nodeRect = node.getBoundingClientRect() const ports = Object.entries(node.ports) .map(([pid, p]) => { const r = p.el.getBoundingClientRect() - const pos = (axis === 'x') + const pos = (orientation == 'vertical') ? (r.left + (r.width / 2) - nodeRect.left) : (r.top + (r.height / 2) - nodeRect.top) return({ pid, pos }) @@ -498,12 +543,10 @@ testEl.classList.add('eic') const toSwap = [] for(let i=0; i layers[lidx-1].includes(nid))) for(let j=i+1; j layers[lidx-1].includes(nid))) const link1 = (pnid1) ? this.getLink(pnid1, nid1) : null const link2 = (pnid2) ? this.getLink(pnid2, nid2) : null @@ -511,12 +554,18 @@ testEl.classList.add('eic') const p2 = adjIndex(pnid2, link2?.from?.[1]) const c1 = adjIndex(nid1, link1?.to?.[1]) const c2 = adjIndex(nid2, link2?.to?.[1]) + if(((p1 - p2) * (c1 - c2)) < 0) { // crossing (now refined by per-port ordering) toSwap.push([i, j]) } } } swap(layer, toSwap) + // Keep bases in sync with the new order so later layers use the updated positions + for(const [idx, nid] of layer.entries()){ + if(!indexes[nid]) indexes[nid] = { base: idx, ports: {} } + else indexes[nid].base = idx + } } } @@ -561,7 +610,11 @@ testEl.classList.add('eic') getLink(nid1, nid2){ - return(this.flow.links.find(item => ((item.from[0]==nid1) && (item.to[0]==nid2)))) + const real = this.flow.links.find(item => ((item.from[0]==nid1) && (item.to[0]==nid2))) + if(real) return(real) + const v = this._virtualLinks?.get(`${nid1}__${nid2}`) + if(v) return(v) + return(null) } buildGraphStructures(nodes, links, includeLinkIndexes = false) {