85 lines
8.7 KiB
Plaintext
85 lines
8.7 KiB
Plaintext
component FlowCanvas {
|
|
outputs { select(payload: object) connect(payload: object) move(payload: object) add(payload: object) }
|
|
props {
|
|
nodes=[]
|
|
palette=[]
|
|
activeNode=""
|
|
label="Workflow"
|
|
arrangeLabel="Arrange nodes"
|
|
connectLabel="Connect"
|
|
color="primary"
|
|
size="default"
|
|
class=""
|
|
}
|
|
state currentNodes = nodes
|
|
state selected = ""
|
|
state pending = null
|
|
state drag = null
|
|
functions {
|
|
shared function findNode(list,id) {for(var i=0;i<list.length;i++){if(list[i].id===id){return list[i];}}return null;}
|
|
shared function rows() {
|
|
var result=[];
|
|
for(var i=0;i<nodes.length;i++){var base=nodes[i];var saved=findNode(currentNodes,base.id);var n=saved || base;result.push({id:base.id,label:base.label,x:n.x ?? 40+i*280,y:n.y ?? 60+(i%2)*100,ports:n.ports || []});}return result;
|
|
}
|
|
shared function stageWidth() {var width=1000;var list=rows();for(var i=0;i<list.length;i++){width=Math.max(width,list[i].x+300);}return width;}
|
|
shared function stageHeight() {var height=460;var list=rows();for(var i=0;i<list.length;i++){height=Math.max(height,list[i].y+180);}return height;}
|
|
shared function paths() {
|
|
var list=rows();var edges=[];
|
|
for(var j=0;j<list.length;j++){var node=list[j];for(var i=0;i<node.ports.length;i++){var port=node.ports[i];var target=findNode(list,port.target);if(target){var x=node.x+230;var y=node.y+62+i*32;var tx=target.x;var ty=target.y+25;edges.push({id:node.id+':'+port.key,path:'M '+x+' '+y+' C '+(x+70)+' '+y+' '+(tx-70)+' '+ty+' '+tx+' '+ty});}}}return edges;
|
|
}
|
|
client function choose(id,event) {
|
|
if(pending){if(pending.source!==id){var list=rows();for(var j=0;j<list.length;j++){var node=list[j];if(node.id===pending.source){var ports=[];for(var i=0;i<node.ports.length;i++){var port=node.ports[i];ports.push({key:port.key,label:port.label,target:port.key===pending.port ? id : port.target});}node.ports=ports;}}currentNodes=list;output.connect({source:pending.source,port:pending.port,target:id,sourceEvent:event});}pending=null;}
|
|
selected=id;output.select({id:id,sourceEvent:event});
|
|
}
|
|
client function begin(node,event) {
|
|
if(event.button!==0 || pending){return;}
|
|
event.currentTarget.closest('.wrn-flow-viewport').setPointerCapture(event.pointerId);drag={id:node.id,startX:event.clientX,startY:event.clientY,x:node.x,y:node.y};
|
|
}
|
|
client function moving(event) {
|
|
if(!drag){return;}var list=rows();for(var i=0;i<list.length;i++){var n=list[i];if(n.id===drag.id){list[i]={id:n.id,label:n.label,x:Math.max(16,drag.x+event.clientX-drag.startX),y:Math.max(16,drag.y+event.clientY-drag.startY),ports:n.ports};}}currentNodes=list;
|
|
}
|
|
client function end(event) {if(!drag){return;}var nodeId=drag.id;drag=null;choose(nodeId,event);output.move({nodes:currentNodes,sourceEvent:event});}
|
|
client function key(nodeId,event) {
|
|
if(event.key==='Escape'){pending=null;return;}
|
|
var delta={ArrowLeft:[-20,0],ArrowRight:[20,0],ArrowUp:[0,-20],ArrowDown:[0,20]}[event.key];
|
|
if(!delta){return;}event.preventDefault();var list=rows();for(var i=0;i<list.length;i++){var n=list[i];if(n.id===nodeId){list[i]={id:n.id,label:n.label,x:Math.max(16,n.x+delta[0]),y:Math.max(16,n.y+delta[1]),ports:n.ports};}}currentNodes=list;output.move({nodes:currentNodes,sourceEvent:event});
|
|
}
|
|
client function arrange(event) {var list=rows();for(var i=0;i<list.length;i++){list[i].x=40+i*280;list[i].y=60+(i%2)*100;}currentNodes=list;output.move({nodes:currentNodes,sourceEvent:event});}
|
|
client function paletteDrag(kind,event) {event.dataTransfer.setData('application/x-wrn-flow-node',kind);event.dataTransfer.effectAllowed='copy';}
|
|
client function drop(event) {event.preventDefault();var kind=event.dataTransfer.getData('application/x-wrn-flow-node');for(var i=0;i<palette.length;i++){if(palette[i].kind===kind){output.add({kind:kind,sourceEvent:event});return;}}}
|
|
|
|
}
|
|
view {
|
|
<section {...attrs} class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-flow {class}" data-ui-component="FlowCanvas" aria-label="{label}" data-wrn-owner="FlowCanvas" data-wrn-part="root" data-wrn-component="FlowCanvas" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">
|
|
<div class="wrn-flow-toolbar" data-wrn-owner="FlowCanvas" data-wrn-part="toolbar" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">{#each palette as item}<button type="button" draggable="true" @dragstart="paletteDrag(item.kind,event)" @click="output.add({kind:item.kind,sourceEvent:event})" data-wrn-owner="FlowCanvas" data-wrn-part="button" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">{item.label}</button>{/each}<button type="button" @click="arrange(event)" data-wrn-owner="FlowCanvas" data-wrn-part="button" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">{arrangeLabel}</button></div>
|
|
<div class="wrn-flow-viewport" @dragover="event.preventDefault()" @drop="drop(event)" @pointermove="moving(event)" @pointerup="end(event)" @pointercancel="end(event)" data-wrn-owner="FlowCanvas" data-wrn-part="viewport" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">
|
|
<div class="wrn-flow-stage" style="min-width: {stageWidth(currentNodes,nodes)}px; min-height: {stageHeight(currentNodes,nodes)}px" data-wrn-owner="FlowCanvas" data-wrn-part="stage" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">
|
|
<svg class="wrn-flow-links" aria-hidden="true" data-wrn-owner="FlowCanvas" data-wrn-part="links" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">{#each paths(currentNodes,nodes) as edge}<path d="{edge.path}" />{/each}</svg>
|
|
{#each rows(currentNodes,nodes) as node}<div class="wrn-flow-node" style="left: {node.x}px; top: {node.y}px" data-selected="{(activeNode || selected)===node.id}" data-wrn-owner="FlowCanvas" data-wrn-part="node" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">
|
|
<button type="button" class="wrn-flow-node-title" data-flow-node-id="{node.id}" aria-label="{node.label+' · '+node.id}" aria-pressed="{(activeNode || selected)===node.id}" @click="choose(node.id,event)" @pointerdown="begin(node,event)" @keydown="key(node.id,event)" data-wrn-owner="FlowCanvas" data-wrn-part="nodeTitle" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}"><span>{node.label}</span><small>{node.id}</small></button>
|
|
{#each node.ports as port}<button type="button" class="wrn-flow-port" data-pending="{pending?.source===node.id && pending?.port===port.key}" aria-label="{connectLabel+' '+node.id+' '+port.label}" @click="pending={source:node.id,port:port.key}" data-wrn-owner="FlowCanvas" data-wrn-part="port" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">{port.label}<span aria-hidden="true"></span></button>{/each}
|
|
</div>{/each}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
}
|
|
style {
|
|
.wrn-flow{border:1px solid var(--wrn-color-border);border-radius:var(--wrn-radius-lg,12px);background:var(--wrn-color-surface);overflow:hidden}
|
|
.wrn-flow-toolbar{display:flex;gap:8px;flex-wrap:wrap;padding:12px;border-bottom:1px solid var(--wrn-color-border)}
|
|
.wrn-flow-toolbar button{padding:8px 12px;border:1px solid var(--wrn-color-border);border-radius:8px;color:var(--wrn-color-text);background:var(--wrn-color-surface-muted);cursor:pointer}
|
|
.wrn-flow-toolbar button:last-child{margin-inline-start:auto}
|
|
.wrn-flow-viewport{overflow:auto;max-height:65vh;background-image:radial-gradient(var(--wrn-color-border) 1px,transparent 1px);background-size:20px 20px}
|
|
.wrn-flow-stage{position:relative;isolation:isolate}
|
|
.wrn-flow-links{position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none}
|
|
.wrn-flow-links path{fill:none;stroke:var(--wrn-color-primary,#0e9db6);stroke-width:2}
|
|
.wrn-flow-node{position:absolute;width:230px;border:1px solid var(--wrn-color-border);border-radius:12px;background:var(--wrn-color-surface);box-shadow:0 2px 8px #0002;overflow:hidden}
|
|
.wrn-flow-node[data-selected=true]{border-color:var(--wrn-color-primary);box-shadow:0 0 0 2px color-mix(in srgb,var(--wrn-color-primary) 20%,transparent)}
|
|
.wrn-flow-node-title{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;padding:14px;border:0;color:var(--wrn-color-text);background:transparent;touch-action:none;cursor:grab;text-align:start}
|
|
.wrn-flow-node-title small{font-size:11px;color:var(--wrn-color-text-muted)}
|
|
.wrn-flow-port{display:flex;align-items:center;justify-content:space-between;width:100%;padding:6px 14px;border:0;border-top:1px solid var(--wrn-color-border);background:transparent;color:var(--wrn-color-text-muted);cursor:crosshair}
|
|
.wrn-flow-port span{width:9px;height:9px;border-radius:50%;background:var(--wrn-color-primary)}
|
|
.wrn-flow-port[data-pending=true]{color:var(--wrn-color-primary);background:var(--wrn-color-surface-muted)}
|
|
.wrn-flow button:focus-visible{outline:2px solid var(--wrn-color-primary);outline-offset:-3px}
|
|
}
|
|
}
|