|
18 | 18 | import StartNode from "./StartNode.svelte"; |
19 | 19 | import EndNode from "./EndNode.svelte"; |
20 | 20 | import StepNode from "./StepNode.svelte"; |
| 21 | + import BreakpointNode from "./BreakpointNode.svelte"; |
| 22 | + import { useDragDrop } from "./DragDropProvider.svelte"; |
21 | 23 | import { MarkerType, Position } from "@xyflow/system"; |
22 | 24 | import { onMount, tick } from "svelte"; |
23 | 25 |
|
|
26 | 28 | edges?: Edge[]; |
27 | 29 | onClick?: (nodeId: string) => void; |
28 | 30 | onExport?: any; |
29 | | - } |
| 31 | + }; |
30 | 32 |
|
31 | 33 | /* A mapping of custom node names to their implementation. The names used in |
32 | 34 | mapping mirror the ManifestKind enum names used in the CM application. |
|
35 | 37 | start: StartNode, |
36 | 38 | end: EndNode, |
37 | 39 | step: StepNode, |
| 40 | + breakpoint: BreakpointNode, |
38 | 41 | } as any as NodeTypes; |
39 | 42 |
|
40 | 43 | /* the $props rune defines the inputs to the component, which can be used as |
41 | 44 | arguments or attributes when the component is initialized. |
42 | 45 | */ |
43 | | - let { |
44 | | - nodes, |
45 | | - edges, |
46 | | - onClick = null, |
47 | | - onExport = null, |
48 | | - }: CanvasProps = $props(); |
| 46 | + let { nodes, edges, onClick = null, onExport = null }: CanvasProps = $props(); |
49 | 47 |
|
50 | 48 | let id = $derived(nodes.length + 1); |
51 | 49 |
|
52 | 50 | // functions provided by the Svelte-Flow |
53 | 51 | const { screenToFlowPosition, fitView } = useSvelteFlow(); |
54 | 52 |
|
55 | 53 | const getId = () => `${id++}`; |
| 54 | + const type = useDragDrop(); |
| 55 | + const onDragStart = (event: DragEvent, nodeType: string) => { |
| 56 | + if (!event.dataTransfer) { |
| 57 | + return null; |
| 58 | + } |
| 59 | + type.current = nodeType; |
| 60 | + event.dataTransfer.effectAllowed = "move"; |
| 61 | + }; |
| 62 | + const onDragOver = (event: DragEvent) => { |
| 63 | + event.preventDefault(); |
| 64 | + if (event.dataTransfer) { |
| 65 | + event.dataTransfer.dropEffect = "move"; |
| 66 | + } |
| 67 | + }; |
| 68 | + const onDrop = (event: DragEvent) => { |
| 69 | + const id = getId(); |
| 70 | + event.preventDefault(); |
| 71 | + if (!type.current) { |
| 72 | + return; |
| 73 | + } |
| 74 | + const position = screenToFlowPosition({ |
| 75 | + x: event.clientX, |
| 76 | + y: event.clientY, |
| 77 | + }); |
| 78 | + const newNode = { |
| 79 | + type: type.current, |
| 80 | + id, |
| 81 | + position, |
| 82 | + data: { name: `${type.current}_${id}`, handleClick: onClick }, |
| 83 | + } satisfies Node; |
| 84 | + nodes = [...nodes, newNode]; |
| 85 | + }; |
56 | 86 |
|
57 | 87 | const handleConnectEnd: OnConnectEnd = (event, connectionState) => { |
58 | 88 | if (connectionState.isValid) return; |
|
162 | 192 | }); |
163 | 193 | </script> |
164 | 194 |
|
165 | | -<SvelteFlow |
166 | | - bind:nodes |
167 | | - bind:edges |
168 | | - {nodeTypes} |
169 | | - {onClick} |
170 | | - fitView |
171 | | - onconnectend={handleConnectEnd} |
172 | | - proOptions={{ hideAttribution: true }} |
173 | | - nodeOrigin={[0.5, 0.5]} |
174 | | -> |
175 | | - <Controls /> |
176 | | - <Background variant={BackgroundVariant.Dots} /> |
177 | | - <MiniMap nodeStrokeWidth={3} /> |
178 | | - <Panel position="top-right"> |
179 | | - <button class="panel-btn" onclick={() => onLayout("TB")} |
180 | | - >Vertical Layout</button |
181 | | - > |
182 | | - <button class="panel-btn" onclick={() => onLayout("LR")} |
183 | | - >Horizontal Layout</button |
184 | | - > |
185 | | - </Panel> |
186 | | -</SvelteFlow> |
| 195 | +<div class="cmcanvas"> |
| 196 | + <SvelteFlow |
| 197 | + bind:nodes |
| 198 | + bind:edges |
| 199 | + {nodeTypes} |
| 200 | + {onClick} |
| 201 | + fitView |
| 202 | + onconnectend={handleConnectEnd} |
| 203 | + ondragover={onDragOver} |
| 204 | + ondrop={onDrop} |
| 205 | + proOptions={{ hideAttribution: true }} |
| 206 | + nodeOrigin={[0.5, 0.5]} |
| 207 | + > |
| 208 | + <Controls /> |
| 209 | + <Background variant={BackgroundVariant.Dots} /> |
| 210 | + <MiniMap nodeStrokeWidth={3} /> |
| 211 | + <Panel position="top-left"> |
| 212 | + <!-- svelte-ignore a11y_click_events_have_key_events --> |
| 213 | + <div class="panel-container"> |
| 214 | + <div |
| 215 | + class="panel-icon clickable" |
| 216 | + role="button" |
| 217 | + tabindex="0" |
| 218 | + onclick={() => onLayout("TB")} |
| 219 | + title="Layout graph top-to-bottom" |
| 220 | + > |
| 221 | + <svg |
| 222 | + class="panel-icon" |
| 223 | + xmlns="http://www.w3.org/2000/svg" |
| 224 | + viewBox="0 -960 960 960" |
| 225 | + ><path |
| 226 | + d="m480-220 160-160-56-56-64 64v-216l64 64 56-56-160-160-160 160 56 56 64-64v216l-64-64-56 56 160 160Zm0 140q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q133 0 226.5-93.5T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 133 93.5 226.5T480-160Zm0-320Z" |
| 227 | + /></svg |
| 228 | + > |
| 229 | + </div> |
| 230 | + <div |
| 231 | + class="panel-icon clickable" |
| 232 | + role="button" |
| 233 | + tabindex="0" |
| 234 | + onclick={() => onLayout("LR")} |
| 235 | + title="Layout graph left-to-right" |
| 236 | + > |
| 237 | + <svg |
| 238 | + class="panel-icon" |
| 239 | + xmlns="http://www.w3.org/2000/svg" |
| 240 | + viewBox="0 -960 960 960" |
| 241 | + ><path |
| 242 | + d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q133 0 226.5-93.5T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 133 93.5 226.5T480-160Zm0-320ZM380-320l56-56-64-64h216l-64 64 56 56 160-160-160-160-56 56 64 64H372l64-64-56-56-160 160 160 160Z" |
| 243 | + /></svg |
| 244 | + > |
| 245 | + </div> |
| 246 | + </div> |
| 247 | + </Panel> |
| 248 | + <Panel position="top-right"> |
| 249 | + <div class="panel-container"> |
| 250 | + <svg |
| 251 | + class="panel-icon" |
| 252 | + xmlns="http://www.w3.org/2000/svg" |
| 253 | + viewBox="0 -960 960 960" |
| 254 | + ><path |
| 255 | + d="M440-280h80v-160h160v-80H520v-160h-80v160H280v80h160v160Zm40 200q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z" |
| 256 | + /></svg |
| 257 | + > |
| 258 | + <div |
| 259 | + class="draggable panel-btn" |
| 260 | + role="button" |
| 261 | + tabindex="0" |
| 262 | + draggable={true} |
| 263 | + ondragstart={(event) => onDragStart(event, "step")} |
| 264 | + title="Drag to create a Step" |
| 265 | + > |
| 266 | + Step |
| 267 | + </div> |
| 268 | + <div |
| 269 | + class="draggable panel-btn" |
| 270 | + role="button" |
| 271 | + tabindex="0" |
| 272 | + draggable={true} |
| 273 | + ondragstart={(event) => onDragStart(event, "breakpoint")} |
| 274 | + title="Drag to create a Breakpoint" |
| 275 | + > |
| 276 | + Breakpoint |
| 277 | + </div> |
| 278 | + </div> |
| 279 | + </Panel> |
| 280 | + </SvelteFlow> |
| 281 | +</div> |
187 | 282 |
|
188 | 283 | <style> |
| 284 | + .cmcanvas { |
| 285 | + height: 100%; |
| 286 | + display: flex; |
| 287 | + flex-direction: column-reverse; |
| 288 | + } |
189 | 289 | :global(.svelte-flow__handle) { |
190 | 290 | opacity: 0; |
191 | 291 | transition: opacity 0.2s; |
|
218 | 318 | border: 2px solid var(--rubin-color-violet-light); |
219 | 319 | } |
220 | 320 |
|
| 321 | + .panel-container { |
| 322 | + font-family: "Source Sans Pro", "Roboto", sans-serif; |
| 323 | + display: flex; |
| 324 | + align-items: center; |
| 325 | + justify-content: center; |
| 326 | + } |
| 327 | +
|
221 | 328 | .panel-btn { |
222 | 329 | padding: 8px 16px; |
223 | 330 | background: var(--rubin-color-white-light); |
224 | 331 | border: 1px solid var(--rubin-color-grey-light); |
225 | 332 | border-radius: 4px; |
226 | | - cursor: pointer; |
227 | 333 | font-size: 14px; |
228 | 334 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
| 335 | + user-select: none; |
| 336 | + font-weight: 500; |
| 337 | + font-style: italic; |
229 | 338 | } |
230 | 339 |
|
231 | 340 | .panel-btn:hover { |
232 | 341 | background: var(--rubin-color-white-dark); |
233 | 342 | } |
| 343 | +
|
| 344 | + .panel-icon { |
| 345 | + display: block; |
| 346 | + flex-shrink: 0; |
| 347 | + height: 32px; |
| 348 | + width: 32px; |
| 349 | + fill: var(--rubin-color-black-light); |
| 350 | + } |
| 351 | +
|
| 352 | + .panel-icon:hover { |
| 353 | + background: var(--rubin-color-white-dark); |
| 354 | + } |
| 355 | +
|
| 356 | + .draggable { |
| 357 | + cursor: grab; |
| 358 | + -webkit-user-drag: element; |
| 359 | + -webkit-transform: translateZ(0); |
| 360 | + } |
| 361 | +
|
| 362 | + .draggable:active { |
| 363 | + cursor: grabbing; |
| 364 | + } |
| 365 | +
|
| 366 | + .clickable { |
| 367 | + cursor: pointer; |
| 368 | + } |
234 | 369 | </style> |
0 commit comments