Skip to content

Commit fe4fb33

Browse files
authored
feat: Allow to customization the connections (#29)
* feat(connection): Allow to customization connection * chore: removed mixins and replaced them with classes for easier typing * chore: cleanup connection to better DX
1 parent 22e3d1f commit fe4fb33

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+3387
-1207
lines changed

.eslintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"extends": [
33
"@gravity-ui/eslint-config",
44
"@gravity-ui/eslint-config/import-order",
5-
// "@gravity-ui/eslint-config/prettier",
5+
"@gravity-ui/eslint-config/prettier",
66
"prettier"
77
],
88
"parserOptions": {

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ node_modules
22
.parcel-cache
33
dist
44
.idea
5+
.vscode
56
.DS_Store
67
storybook-static
78
.tsbuildinfo

package-lock.json

Lines changed: 17 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@
103103
"size-limit": "^10.0.1",
104104
"storybook": "^8.1.11",
105105
"ts-node": "^10.9.2",
106-
"typescript": "^5.5.4"
106+
"typescript": "^5.5.4",
107+
"web-worker": "^1.3.0",
108+
"elkjs": "^0.9.3"
107109
}
108110
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { Component, TComponentContext, TComponentProps, TComponentState } from "../../../lib/Component";
2+
3+
type TEventedComponentListener = Component | ((e: Event) => void);
4+
5+
const listeners = new WeakMap<Component, Map<string, Set<TEventedComponentListener>>>();
6+
7+
export class EventedComponent<
8+
Props extends TComponentProps = TComponentProps,
9+
State extends TComponentState = TComponentState,
10+
Context extends TComponentContext = TComponentContext,
11+
> extends Component<Props, State, Context> {
12+
public readonly cursor?: string;
13+
14+
private get events() {
15+
if (!listeners.has(this)) {
16+
listeners.set(this, new Map());
17+
}
18+
return listeners.get(this);
19+
}
20+
21+
protected unmount() {
22+
listeners.delete(this);
23+
super.unmount();
24+
}
25+
26+
protected handleEvent(_: Event) {
27+
// noop
28+
}
29+
30+
public listenEvents(events: string[], cbOrObject: TEventedComponentListener = this) {
31+
const unsubs = events.map((eventName) => {
32+
return this.addEventListener(eventName, cbOrObject);
33+
});
34+
return unsubs;
35+
}
36+
37+
public addEventListener(type: string, cbOrObject: TEventedComponentListener) {
38+
const cbs = this.events.get(type) || new Set();
39+
cbs.add(cbOrObject);
40+
this.events.set(type, cbs);
41+
return () => this.removeEventListener(type, cbOrObject);
42+
}
43+
44+
public removeEventListener(type: string, cbOrObject: TEventedComponentListener) {
45+
const cbs = this.events.get(type);
46+
if (cbs) {
47+
cbs.delete(cbOrObject);
48+
}
49+
}
50+
51+
public _fireEvent(cmp: Component, event: Event) {
52+
const handlers = listeners.get(cmp)?.get?.(event.type);
53+
54+
handlers?.forEach((cb) => {
55+
if (typeof cb === "function") {
56+
cb(event);
57+
} else if (cb instanceof Component && "handleEvent" in cb && typeof cb.handleEvent === "function") {
58+
cb.handleEvent?.(event);
59+
}
60+
});
61+
}
62+
63+
public dispatchEvent(event: Event): boolean {
64+
const bubbles = event.bubbles || false;
65+
66+
if (bubbles) {
67+
return this._dipping(this, event);
68+
} else if (this._hasListener(this, event.type)) {
69+
this._fireEvent(this, event);
70+
return false;
71+
}
72+
return false;
73+
}
74+
75+
public _dipping(startParent: Component, event: Event) {
76+
let stopPropagation = false;
77+
let parent: Component = startParent;
78+
event.stopPropagation = () => {
79+
stopPropagation = true;
80+
};
81+
82+
do {
83+
this._fireEvent(parent, event);
84+
if (stopPropagation) {
85+
return false;
86+
}
87+
parent = parent.getParent() as Component;
88+
} while (parent);
89+
90+
return true;
91+
}
92+
93+
public _hasListener(comp: EventedComponent, type: string) {
94+
return listeners.get(comp)?.has?.(type);
95+
}
96+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Signal } from "@preact/signals-core";
2+
3+
import { Graph } from "../../../graph";
4+
import { Component } from "../../../lib";
5+
import { TComponentContext, TComponentProps, TComponentState } from "../../../lib/Component";
6+
import { HitBox, HitBoxData } from "../../../services/HitTest";
7+
import { EventedComponent } from "../EventedComponent/EventedComponent";
8+
import { TGraphLayerContext } from "../layers/graphLayer/GraphLayer";
9+
10+
export type GraphComponentContext = TComponentContext &
11+
TGraphLayerContext & {
12+
graph: Graph;
13+
};
14+
15+
export class GraphComponent<
16+
Props extends TComponentProps = TComponentProps,
17+
State extends TComponentState = TComponentState,
18+
Context extends GraphComponentContext = GraphComponentContext,
19+
> extends EventedComponent<Props, State, Context> {
20+
public hitBox: HitBox;
21+
22+
private unsubscribe: (() => void)[] = [];
23+
24+
constructor(props: Props, parent: Component) {
25+
super(props, parent);
26+
this.hitBox = new HitBox(this, this.context.graph.hitTest);
27+
}
28+
29+
protected subscribeSignal<T>(signal: Signal<T>, cb: (v: T) => void) {
30+
this.unsubscribe.push(signal.subscribe(cb));
31+
}
32+
33+
protected unmount() {
34+
super.unmount();
35+
this.unsubscribe.forEach((cb) => cb());
36+
this.destroyHitBox();
37+
}
38+
39+
public setHitBox(minX: number, minY: number, maxX: number, maxY: number, force?: boolean) {
40+
this.hitBox.update(minX, minY, maxX, maxY, force);
41+
}
42+
43+
protected willIterate(): void {
44+
super.willIterate();
45+
if (!this.firstIterate) {
46+
this.shouldRender = this.isVisible();
47+
}
48+
}
49+
50+
protected isVisible() {
51+
return this.context.camera.isRectVisible(...this.getHitBox());
52+
}
53+
54+
public getHitBox() {
55+
return this.hitBox.getRect();
56+
}
57+
58+
public removeHitBox() {
59+
this.hitBox.remove();
60+
}
61+
62+
public destroyHitBox() {
63+
this.hitBox.destroy();
64+
}
65+
66+
public onHitBox(_: HitBoxData) {
67+
return this.isIterated();
68+
}
69+
}

src/components/canvas/anchors/index.ts

Lines changed: 17 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import { EventedComponent } from "../../../mixins/withEvents";
2-
import { withHitTest } from "../../../mixins/withHitTest";
31
import { ECameraScaleLevel } from "../../../services/camera/CameraService";
42
import { frameDebouncer } from "../../../services/optimizations/frameDebouncer";
53
import { AnchorState, EAnchorType } from "../../../store/anchor/Anchor";
64
import { TBlockId } from "../../../store/block/Block";
75
import { selectBlockAnchor } from "../../../store/block/selectors";
86
import { TPoint } from "../../../utils/types/shapes";
7+
import { GraphComponent } from "../GraphComponent";
98
import { GraphLayer, TGraphLayerContext } from "../layers/graphLayer/GraphLayer";
109

1110
export type TAnchor = {
@@ -28,7 +27,7 @@ type TAnchorState = {
2827
selected: boolean;
2928
};
3029

31-
export class Anchor extends withHitTest(EventedComponent) {
30+
export class Anchor extends GraphComponent<TAnchorProps, TAnchorState> {
3231
public readonly cursor = "pointer";
3332

3433
public get zIndex() {
@@ -48,23 +47,24 @@ export class Anchor extends withHitTest(EventedComponent) {
4847

4948
private hitBoxHash: string;
5049

51-
private debouncedSetHitBox: (...args: any[]) => void;
52-
53-
protected readonly unsubscribe: (() => void)[] = [];
50+
private debouncedSetHitBox = frameDebouncer.add(
51+
() => {
52+
const { x, y } = this.props.getPosition(this.props);
53+
this.setHitBox(x - this.shift, y - this.shift, x + this.shift, y + this.shift);
54+
},
55+
{
56+
delay: 4,
57+
lightFrame: true,
58+
}
59+
);
5460

5561
constructor(props: TAnchorProps, parent: GraphLayer) {
5662
super(props, parent);
5763
this.state = { size: props.size, raised: false, selected: false };
5864

5965
this.connectedState = selectBlockAnchor(this.context.graph, props.blockId, props.id);
60-
61-
if (this.connectedState) {
62-
this.unsubscribe = this.subscribe();
63-
}
64-
65-
this.debouncedSetHitBox = frameDebouncer.add(this.bindedSetHitBox.bind(this), {
66-
delay: 4,
67-
lightFrame: true,
66+
this.subscribeSignal(this.connectedState.$selected, (selected) => {
67+
this.setState({ selected });
6868
});
6969

7070
this.addEventListener("click", this);
@@ -80,30 +80,13 @@ export class Anchor extends withHitTest(EventedComponent) {
8080
return this.props.getPosition(this.props);
8181
}
8282

83-
protected subscribe() {
84-
return [
85-
this.connectedState.$selected.subscribe((selected) => {
86-
this.setState({ selected });
87-
}),
88-
];
89-
}
90-
91-
protected unmount() {
92-
this.unsubscribe.forEach((reactionDisposer) => reactionDisposer());
93-
94-
super.unmount();
95-
}
96-
9783
public toggleSelected() {
9884
this.connectedState.setSelection(!this.state.selected);
9985
}
10086

101-
public willIterate() {
102-
super.willIterate();
103-
104-
const { x, y, width, height } = this.hitBox.getRect();
105-
106-
this.shouldRender = width && height ? this.context.camera.isRectVisible(x, y, width, height) : true;
87+
protected isVisible() {
88+
const params = this.getHitBox();
89+
return params ? this.context.camera.isRectVisible(...params) : true;
10790
}
10891

10992
public didIterate(): void {
@@ -137,11 +120,6 @@ export class Anchor extends withHitTest(EventedComponent) {
137120
}
138121
}
139122

140-
public bindedSetHitBox() {
141-
const { x, y } = this.props.getPosition(this.props);
142-
this.setHitBox(x - this.shift, y - this.shift, x + this.shift, y + this.shift);
143-
}
144-
145123
private computeRenderSize(size: number, raised: boolean) {
146124
if (raised) {
147125
this.setState({ size: size * 1.8 });

0 commit comments

Comments
 (0)