Skip to content

Commit ec53993

Browse files
authored
feat(GraphComponent, HitBox): introduce interactive and non-boundary components (#130)
* feat: introduce interactive and non-boundary components.
1 parent d824c45 commit ec53993

File tree

10 files changed

+594
-52
lines changed

10 files changed

+594
-52
lines changed

src/components/canvas/EventedComponent/EventedComponent.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,35 @@ type TEventedComponentListener = Component | ((e: Event) => void);
44

55
const listeners = new WeakMap<Component, Map<string, Set<TEventedComponentListener>>>();
66

7+
export type TEventedComponentProps = TComponentProps & { interactive?: boolean };
8+
79
export class EventedComponent<
8-
Props extends TComponentProps = TComponentProps,
10+
Props extends TEventedComponentProps = TEventedComponentProps,
911
State extends TComponentState = TComponentState,
1012
Context extends TComponentContext = TComponentContext,
1113
> extends Component<Props, State, Context> {
1214
public readonly evented: boolean = true;
1315

1416
public cursor?: string;
1517

18+
constructor(props: Props, parent: Component) {
19+
super(
20+
{
21+
...props,
22+
interactive: props.interactive ?? true,
23+
},
24+
parent
25+
);
26+
}
27+
28+
public isInteractive() {
29+
return this.props.interactive;
30+
}
31+
32+
public setInteractive(interactive: boolean) {
33+
this.setProps({ interactive });
34+
}
35+
1636
private get events() {
1737
if (!listeners.has(this)) {
1838
listeners.set(this, new Map());
@@ -50,7 +70,10 @@ export class EventedComponent<
5070
}
5171
}
5272

53-
public _fireEvent(cmp: Component, event: Event) {
73+
protected _fireEvent(cmp: Component, event: Event) {
74+
if (cmp instanceof EventedComponent && !cmp.isInteractive?.()) {
75+
return;
76+
}
5477
const handlers = listeners.get(cmp)?.get?.(event.type);
5578

5679
handlers?.forEach((cb) => {
@@ -65,7 +88,7 @@ export class EventedComponent<
6588
public dispatchEvent(event: Event): boolean {
6689
const bubbles = event.bubbles || false;
6790

68-
if (bubbles) {
91+
if (bubbles || !this.isInteractive()) {
6992
return this._dipping(this, event);
7093
} else if (this._hasListener(this, event.type)) {
7194
this._fireEvent(this, event);
@@ -74,7 +97,7 @@ export class EventedComponent<
7497
return false;
7598
}
7699

77-
public _dipping(startParent: Component, event: Event) {
100+
protected _dipping(startParent: Component, event: Event) {
78101
let stopPropagation = false;
79102
let parent: Component = startParent;
80103
event.stopPropagation = () => {
@@ -92,7 +115,7 @@ export class EventedComponent<
92115
return true;
93116
}
94117

95-
public _hasListener(comp: EventedComponent, type: string) {
118+
protected _hasListener(comp: EventedComponent, type: string) {
96119
return listeners.get(comp)?.has?.(type);
97120
}
98121
}

src/components/canvas/GraphComponent/index.tsx

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,16 @@ import { TGraphLayerContext } from "../layers/graphLayer/GraphLayer";
1414
export type GraphComponentContext = TComponentContext &
1515
TGraphLayerContext & {
1616
graph: Graph;
17+
affectsUsableRect?: boolean;
1718
};
1819

20+
export type TGraphComponentProps = TComponentProps & {
21+
interactive?: boolean;
22+
affectsUsableRect?: boolean;
23+
};
24+
1925
export class GraphComponent<
20-
Props extends TComponentProps = TComponentProps,
26+
Props extends TGraphComponentProps = TGraphComponentProps,
2127
State extends TComponentState = TComponentState,
2228
Context extends GraphComponentContext = GraphComponentContext,
2329
> extends EventedComponent<Props, State, Context> {
@@ -27,9 +33,18 @@ export class GraphComponent<
2733

2834
protected ports: Map<TPortId, PortState> = new Map();
2935

36+
public get affectsUsableRect() {
37+
return this.props.affectsUsableRect ?? this.context.affectsUsableRect ?? true;
38+
}
39+
3040
constructor(props: Props, parent: Component) {
3141
super(props, parent);
42+
43+
// Determine affectsUsableRect value: explicit prop > parent context > default (true)
3244
this.hitBox = new HitBox(this, this.context.graph.hitTest);
45+
const affectsUsableRect = props.affectsUsableRect ?? this.context.affectsUsableRect ?? true;
46+
this.setProps({ affectsUsableRect });
47+
this.setContext({ affectsUsableRect });
3348
}
3449

3550
public createPort(id: TPortId) {
@@ -45,6 +60,44 @@ export class GraphComponent<
4560
return this.ports.get(id)!;
4661
}
4762

63+
protected setAffectsUsableRect(affectsUsableRect: boolean) {
64+
this.setProps({ affectsUsableRect });
65+
this.setContext({ affectsUsableRect });
66+
}
67+
68+
protected propsChanged(_nextProps: Props): void {
69+
if (this.affectsUsableRect !== _nextProps.affectsUsableRect) {
70+
this.hitBox.setAffectsUsableRect(_nextProps.affectsUsableRect);
71+
this.setContext({ affectsUsableRect: _nextProps.affectsUsableRect });
72+
}
73+
super.propsChanged(_nextProps);
74+
}
75+
76+
protected contextChanged(_nextContext: Context): void {
77+
// If affectsUsableRect changed in context and there's no explicit prop override
78+
if (
79+
this.firstRender ||
80+
(this.context.affectsUsableRect !== _nextContext.affectsUsableRect && this.props.affectsUsableRect === undefined)
81+
) {
82+
this.hitBox.setAffectsUsableRect(_nextContext.affectsUsableRect);
83+
}
84+
super.contextChanged(_nextContext);
85+
}
86+
87+
public onChange(cb: (v: this) => void) {
88+
return this.addEventListener("graph-component-change", () => {
89+
cb(this);
90+
});
91+
}
92+
93+
protected checkData() {
94+
if (super.checkData()) {
95+
this.dispatchEvent(new Event("graph-component-change"));
96+
return true;
97+
}
98+
return false;
99+
}
100+
48101
protected onDrag({
49102
onDragStart,
50103
onDragUpdate,

src/components/canvas/anchors/index.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { TBlockId } from "../../../store/block/Block";
55
import { selectBlockAnchor } from "../../../store/block/selectors";
66
import { debounce, isMetaKeyEvent } from "../../../utils/functions";
77
import { TPoint } from "../../../utils/types/shapes";
8-
import { GraphComponent } from "../GraphComponent";
8+
import { GraphComponent, TGraphComponentProps } from "../GraphComponent";
99
import { GraphLayer, TGraphLayerContext } from "../layers/graphLayer/GraphLayer";
1010

1111
export type TAnchor = {
@@ -15,12 +15,13 @@ export type TAnchor = {
1515
index?: number;
1616
};
1717

18-
export type TAnchorProps = TAnchor & {
19-
size: number;
20-
lineWidth: number;
21-
zIndex: number;
22-
getPosition: (anchor: TAnchor) => TPoint;
23-
};
18+
export type TAnchorProps = TGraphComponentProps &
19+
TAnchor & {
20+
size: number;
21+
lineWidth: number;
22+
zIndex: number;
23+
getPosition: (anchor: TAnchor) => TPoint;
24+
};
2425

2526
type TAnchorState = {
2627
size: number;

src/components/canvas/blocks/Block.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { signal } from "@preact/signals-core";
22
import cloneDeep from "lodash/cloneDeep";
33
import isObject from "lodash/isObject";
44

5+
import { Component } from "../../../lib/Component";
56
import { ECameraScaleLevel } from "../../../services/camera/CameraService";
67
import { TGraphSettingsConfig } from "../../../store";
78
import { EAnchorType } from "../../../store/anchor/Anchor";
@@ -14,9 +15,9 @@ import { TMeasureTextOptions } from "../../../utils/functions/text";
1415
import { TTExtRect, renderText } from "../../../utils/renderers/text";
1516
import { EVENTS } from "../../../utils/types/events";
1617
import { TPoint, TRect } from "../../../utils/types/shapes";
17-
import { GraphComponent } from "../GraphComponent";
18+
import { GraphComponent, TGraphComponentProps } from "../GraphComponent";
1819
import { Anchor, TAnchor } from "../anchors";
19-
import { GraphLayer, TGraphLayerContext } from "../layers/graphLayer/GraphLayer";
20+
import { TGraphLayerContext } from "../layers/graphLayer/GraphLayer";
2021

2122
import { BlockController } from "./controllers/BlockController";
2223

@@ -48,9 +49,9 @@ export type TBlock<T extends Record<string, unknown> = {}> = {
4849
meta?: T;
4950
};
5051

51-
export type TBlockProps = {
52+
export type TBlockProps = TGraphComponentProps & {
5253
id: TBlockId;
53-
font: string;
54+
font?: string;
5455
};
5556

5657
declare module "../../../graphEvents" {
@@ -123,7 +124,7 @@ export class Block<T extends TBlock = TBlock, Props extends TBlockProps = TBlock
123124

124125
public $viewState = signal<BlockViewState>({ zIndex: 0, order: 0 });
125126

126-
constructor(props: Props, parent: GraphLayer) {
127+
constructor(props: Props, parent: Component) {
127128
super(props, parent);
128129

129130
this.subscribe(props.id);

src/components/canvas/layers/graphLayer/GraphLayer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ export class GraphLayer extends Layer<TGraphLayerProps, TGraphLayerContext> {
226226

227227
private onRootPointerMove(event: MouseEvent) {
228228
if (this.targetComponent !== this.prevTargetComponent) {
229-
if (this.targetComponent?.cursor) {
229+
if (this.targetComponent?.isInteractive() && this.targetComponent?.cursor) {
230230
this.root.style.cursor = this.targetComponent?.cursor;
231231
} else {
232232
this.root.style.removeProperty("cursor");

src/lib/Component.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,11 @@ export class Component<
3434
// noop
3535
}
3636

37-
public setContext<K extends keyof Context>(context: Pick<Context, K>) {
37+
public setContext<K extends keyof Context>(context: Partial<Pick<Context, K>>) {
3838
this.shouldRenderChildren = true;
3939
this.shouldUpdateChildren = true;
40-
super.setContext(context);
40+
super.setContext({ ...this.context, ...context });
41+
this.contextChanged(this.context);
4142
}
4243

4344
public setProps<K extends keyof Props>(props?: Pick<Props, K>) {
@@ -72,20 +73,28 @@ export class Component<
7273
// noop
7374
}
7475

76+
protected contextChanged(_nextContext: Context): void {
77+
// noop
78+
}
79+
7580
protected checkData() {
7681
const data = this.__data;
82+
let updated = false;
7783

7884
if (data.nextProps !== undefined) {
7985
this.propsChanged(data.nextProps);
8086
assign(this.props, data.nextProps);
8187
data.nextProps = undefined;
88+
updated = true;
8289
}
8390

8491
if (data.nextState !== undefined) {
8592
this.stateChanged(data.nextState);
8693
assign(this.state, data.nextState);
8794
data.nextState = undefined;
95+
updated = true;
8896
}
97+
return updated;
8998
}
9099

91100
protected willRender() {

src/lib/CoreComponent.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,19 @@ export class CoreComponent<
9797
}
9898

9999
public setContext<K extends keyof Context>(context: Pick<Context, K>) {
100-
Object.assign(this.context, context);
100+
this.context = Object.assign({}, this.context, context);
101+
102+
// Propagate context changes to all children
103+
const children = this.__comp.children;
104+
const childrenKeys = this.__comp.childrenKeys;
105+
106+
for (let i = 0; i < childrenKeys.length; i += 1) {
107+
const child = children[childrenKeys[i]];
108+
if (child) {
109+
child.setContext(context);
110+
}
111+
}
112+
101113
this.performRender();
102114
}
103115

src/react-components/Block.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,17 @@ export const GraphBlock = <T extends TBlock>({
5757
}
5858
}, [state?.x, state?.y, state?.width, state?.height]);
5959

60+
useEffect(() => {
61+
if (viewState && containerRef.current) {
62+
containerRef.current.style.pointerEvents = viewState.isInteractive() ? "auto" : "none";
63+
return viewState.onChange(() => {
64+
if (containerRef.current) {
65+
containerRef.current.style.pointerEvents = viewState.isInteractive() ? "auto" : "none";
66+
}
67+
});
68+
}
69+
}, [viewState]);
70+
6071
useEffect(() => {
6172
if (viewState && containerRef.current) {
6273
return viewState.$viewState.subscribe(({ zIndex, order }) => {

0 commit comments

Comments
 (0)