Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/components/canvas/anchors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ export class Anchor<T extends TAnchorProps = TAnchorProps> extends GraphComponen
this.computeShift(_nextState, this.props);
this.onPositionChanged();
}
if (this.state.raised !== _nextState.raised) {
this.computeRenderSize(this.props.size, _nextState.raised);
}
super.stateChanged(_nextState);
}

Expand Down Expand Up @@ -182,12 +185,10 @@ export class Anchor<T extends TAnchorProps = TAnchorProps> extends GraphComponen
}
case "mouseenter": {
this.setState({ raised: true });
this.computeRenderSize(this.props.size, true);
break;
}
case "mouseleave": {
this.setState({ raised: false });
this.computeRenderSize(this.props.size, false);
break;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { ESelectionStrategy } from "../../../../services/selection";
import { EAnchorType } from "../../../../store/anchor/Anchor";
import { TBlockId } from "../../../../store/block/Block";
import { PortState } from "../../../../store/connection/port/Port";
import type { Emitter } from "../../../../utils/Emitter";
import { vectorDistance } from "../../../../utils/functions";
import { stopDragListening } from "../../../../utils/functions/dragListener";
import { render } from "../../../../utils/renderers/render";
import { renderSVG } from "../../../../utils/renderers/svgPath";
import { Point, TPoint } from "../../../../utils/types/shapes";
Expand Down Expand Up @@ -117,6 +119,13 @@ declare module "../../../../graphEvents" {
}>
) => void;

"port-connection-cancel": (
event: CustomEvent<{
sourcePort: PortState;
targetPort: PortState;
}>
) => void;

/**
* Port-based event fired when the user releases the mouse button.
* Extends connection-create-drop with direct port references.
Expand Down Expand Up @@ -226,6 +235,18 @@ export class PortConnectionLayer extends Layer<
this.isSnappingTreeOutdated = true;
});

this.context.graph.keyboardService.onPress(
"Escape",
() => {
if (this.currentListener) {
this.cancelNewConnection();
}
},
{
signal: this.eventAbortController.signal,
}
);

super.afterInit();
}

Expand All @@ -237,6 +258,8 @@ export class PortConnectionLayer extends Layer<
this.enabled = false;
};

protected currentListener: Emitter | null = null;

protected handleMouseDown = (nativeEvent: GraphMouseEvent): void => {
if (!this.enabled) {
return;
Expand All @@ -253,7 +276,7 @@ export class PortConnectionLayer extends Layer<
nativeEvent.stopGraphEventPropagation();
}
// DragService will provide world coordinates in callbacks
this.context.graph.dragService.startDrag(
this.currentListener = this.context.graph.dragService.startDrag(
{
onStart: (_event, coords) => {
const point = new Point(coords[0], coords[1]);
Expand Down Expand Up @@ -400,25 +423,25 @@ export class PortConnectionLayer extends Layer<

this.targetPort = newTargetPort;

if (newTargetPort) {
const sourceParams = this.getEventParams(this.sourcePort);
const targetParams = this.getEventParams(newTargetPort);

this.context.graph.executеDefaultEventAction(
"port-connection-create-hover",
{
sourceBlockId: sourceParams.blockId,
sourceAnchorId: sourceParams.anchorId,
targetBlockId: targetParams.blockId,
targetAnchorId: targetParams.anchorId,
sourcePort: this.sourcePort,
targetPort: newTargetPort,
},
() => {
this.selectPort(newTargetPort, true);
const sourceParams = this.getEventParams(this.sourcePort);
const targetParams = this.getEventParams(newTargetPort);

this.context.graph.executеDefaultEventAction(
"port-connection-create-hover",
{
sourceBlockId: sourceParams.blockId,
sourceAnchorId: sourceParams.anchorId,
targetBlockId: targetParams?.blockId,
targetAnchorId: targetParams?.anchorId,
sourcePort: this.sourcePort,
targetPort: newTargetPort || undefined,
},
() => {
if (this.targetPort) {
this.selectPort(this.targetPort, true);
}
);
}
}
);
}
}

Expand All @@ -438,6 +461,26 @@ export class PortConnectionLayer extends Layer<
}
}

protected cancelNewConnection(): void {
if (this.currentListener) {
stopDragListening(this.currentListener);
}
this.startState = null;
this.endState = null;
this.performRender();
this.context.graph.executеDefaultEventAction(
"port-connection-cancel",
{
sourcePort: this.sourcePort,
targetPort: this.targetPort,
},
() => {}
);
// Cleanup ports to prevent stale state
this.sourcePort = undefined;
this.targetPort = undefined;
}

private onEndNewConnection(point: Point): void {
if (!this.sourcePort || !this.startState || !this.endState) {
return;
Expand Down Expand Up @@ -688,10 +731,13 @@ export class PortConnectionLayer extends Layer<
* Get full event parameters from a port
* Includes both legacy parameters (blockId, anchorId) and new port reference
*/
private getEventParams(port: PortState): {
blockId: TBlockId;
protected getEventParams(port?: PortState): {
blockId?: TBlockId;
anchorId?: string;
} {
if (!port) {
return {};
}
const component = port.owner;

if (!component) {
Expand All @@ -711,7 +757,7 @@ export class PortConnectionLayer extends Layer<
};
}

throw new Error("Port owner is not Block or Anchor");
return {};
}

public override unmount(): void {
Expand Down
5 changes: 3 additions & 2 deletions src/react-components/Anchor.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
border: 2px solid var(--graph-block-anchor-border-selected);
transform: scale(var(--graph-block-anchor-hover-scale, 1.2));
}
.graph-block-anchor:hover {
.graph-block-anchor:hover,
.graph-block-anchor.graph-block-anchor-raised {
transform: scale(var(--graph-block-anchor-hover-scale, 1.2));
}

Expand All @@ -29,4 +30,4 @@
/* x,y offset needs to */
top: calc(var(--y) - var(--y-offset));
left: calc(var(--x) - var(--x-offset));
}
}
24 changes: 19 additions & 5 deletions src/react-components/Anchor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export function GraphBlockAnchor({
const anchorContainerRef = React.useRef<HTMLDivElement>(null);
const anchorState = useBlockAnchorState(graph, anchor);
const selected = useSignal(anchorState?.$selected);
const [raised, setRaised] = React.useState(false);

useBlockAnchorPosition(anchorState, anchorContainerRef);

Expand All @@ -34,10 +35,23 @@ export function GraphBlockAnchor({
"graph-block-anchor",
`graph-block-anchor-${anchor.type.toLocaleLowerCase()}`,
`graph-block-position-${position}`,
className,
selected ? "graph-block-anchor-selected" : ""
{
"graph-block-anchor-raised": raised,
"graph-block-anchor-selected": selected,
},
className
);
}, [anchor?.type, position, className, selected]);
}, [anchor?.type, position, className, selected, raised]);

useEffect(() => {
const component = anchorState?.getViewComponent();
if (!component) {
return () => {};
}
return component.onChange(() => {
setRaised(component.state.raised);
});
}, [anchorState]);

useEffect(() => {
if (anchorContainerRef.current) {
Expand All @@ -50,11 +64,11 @@ export function GraphBlockAnchor({
}, [anchorState?.$selected.value]);

if (!anchorState) return null;
const render = typeof children === "function" ? children : () => children;
const layout = typeof children === "function" ? children(anchorState) : children;

return (
<div ref={anchorContainerRef} className={classNames}>
{render(anchorState)}
{layout}
</div>
);
}
25 changes: 25 additions & 0 deletions src/services/KeyboardService/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export class KeyboardService {
return this.$keyboardState.value.altKey;
}

protected keybordEvents = new EventTarget();

protected startListening(): void {
this.graph.layers.$root?.ownerDocument?.addEventListener("keydown", this.handleKeyDown, {
capture: true,
Expand All @@ -80,6 +82,20 @@ export class KeyboardService {
});
}

public onPress(key: string, cb: (event: CustomEvent<KeyboardEvent>) => void, options?: AddEventListenerOptions) {
this.keybordEvents.addEventListener(`press-${key}`, cb, options);
return () => {
this.keybordEvents.removeEventListener(`press-${key}`, cb);
};
}

public onRelease(key: string, cb: (event: CustomEvent<KeyboardEvent>) => void, options?: AddEventListenerOptions) {
this.keybordEvents.addEventListener(`release-${key}`, cb, options);
return () => {
this.keybordEvents.removeEventListener(`release-${key}`, cb);
};
}

protected stopListening(): void {
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
this.unsubscribes = [];
Expand All @@ -102,6 +118,14 @@ export class KeyboardService {
);
}

protected notifyKeyCode(event: KeyboardEvent): void {
if (event.type === "keydown") {
this.keybordEvents.dispatchEvent(new CustomEvent(`press-${event.key}`, { detail: event }));
} else if (event.type === "keyup") {
this.keybordEvents.dispatchEvent(new CustomEvent(`release-${event.key}`, { detail: event }));
}
}

protected handleKeyDown = (event: KeyboardEvent): void => {
if (this.hasChanged(event)) {
this.lastEvent = event;
Expand All @@ -112,6 +136,7 @@ export class KeyboardService {
altKey: event.altKey,
};
}
this.notifyKeyCode(event);
};

protected cleanup(): void {
Expand Down
11 changes: 3 additions & 8 deletions src/services/drag/DragService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,26 +319,21 @@ export class DragService {
* );
* ```
*/
public startDrag(callbacks: DragOperationCallbacks, options: DragOperationOptions = {}): void {
public startDrag(callbacks: DragOperationCallbacks, options: DragOperationOptions = {}) {
const { document: doc, cursor, autopanning = true, stopOnMouseLeave, threshold, initialEvent } = options;
const { onStart, onUpdate, onEnd } = callbacks;

const targetDocument = doc ?? this.graph.getGraphCanvas().ownerDocument;
let initialCoords: [number, number] | null = null;
if (threshold && initialEvent) {
const coords = this.getWorldCoords(initialEvent);
initialCoords = coords;
}

dragListener(targetDocument, {
return dragListener(targetDocument, {
graph: this.graph,
dragCursor: cursor,
autopanning,
stopOnMouseLeave,
threshold,
})
.on(EVENTS.DRAG_START, (event: MouseEvent) => {
onStart?.(event, initialCoords ?? this.getWorldCoords(event));
onStart?.(event, initialEvent ? this.getWorldCoords(initialEvent) : this.getWorldCoords(event));
})
.on(EVENTS.DRAG_UPDATE, (event: MouseEvent) => {
const coords = this.getWorldCoords(event);
Expand Down
3 changes: 0 additions & 3 deletions src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { SelectionService } from "../services/selection/SelectionService";

import { BlockListStore } from "./block/BlocksList";
import { ConnectionsStore } from "./connection/ConnectionList";
import { PortsStore } from "./connection/port/PortList";
import { GroupsListStore } from "./group/GroupsList";
import { GraphEditorSettings } from "./settings";

Expand All @@ -21,8 +20,6 @@ export class RootStore {

public groupsList: GroupsListStore;

public portsList: PortsStore;

public selectionService: SelectionService;

constructor(graph: Graph) {
Expand Down
12 changes: 12 additions & 0 deletions src/utils/functions/dragListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,21 @@ export function dragListener(document: Document | HTMLDivElement | HTMLCanvasEle
{ once: true, capture: true }
);

emitter.on("cancel", () => {
if (started) {
cleanupDragState();
}
finished = true;
cleanup();
});

return emitter;
}

export function stopDragListening(emitter: Emitter) {
emitter.emit("cancel");
}

function mousemove(emitter: Emitter, event: MouseEvent) {
emitter.emit(EVENTS.DRAG_UPDATE, event);
}
Expand Down
Loading