Skip to content

Commit

Permalink
feat: Allow to customization the connections (#29)
Browse files Browse the repository at this point in the history
* feat(connection): Allow to customization connection
* chore: removed mixins and replaced them with classes for easier typing
* chore: cleanup connection to better DX
  • Loading branch information
draedful authored Dec 15, 2024
1 parent 22e3d1f commit fe4fb33
Show file tree
Hide file tree
Showing 70 changed files with 3,387 additions and 1,207 deletions.
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"extends": [
"@gravity-ui/eslint-config",
"@gravity-ui/eslint-config/import-order",
// "@gravity-ui/eslint-config/prettier",
"@gravity-ui/eslint-config/prettier",
"prettier"
],
"parserOptions": {
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ node_modules
.parcel-cache
dist
.idea
.vscode
.DS_Store
storybook-static
.tsbuildinfo
Expand Down
18 changes: 17 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@
"size-limit": "^10.0.1",
"storybook": "^8.1.11",
"ts-node": "^10.9.2",
"typescript": "^5.5.4"
"typescript": "^5.5.4",
"web-worker": "^1.3.0",
"elkjs": "^0.9.3"
}
}
96 changes: 96 additions & 0 deletions src/components/canvas/EventedComponent/EventedComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Component, TComponentContext, TComponentProps, TComponentState } from "../../../lib/Component";

type TEventedComponentListener = Component | ((e: Event) => void);

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

export class EventedComponent<
Props extends TComponentProps = TComponentProps,
State extends TComponentState = TComponentState,
Context extends TComponentContext = TComponentContext,
> extends Component<Props, State, Context> {
public readonly cursor?: string;

private get events() {
if (!listeners.has(this)) {
listeners.set(this, new Map());
}
return listeners.get(this);
}

protected unmount() {
listeners.delete(this);
super.unmount();
}

protected handleEvent(_: Event) {
// noop
}

public listenEvents(events: string[], cbOrObject: TEventedComponentListener = this) {
const unsubs = events.map((eventName) => {
return this.addEventListener(eventName, cbOrObject);
});
return unsubs;
}

public addEventListener(type: string, cbOrObject: TEventedComponentListener) {
const cbs = this.events.get(type) || new Set();
cbs.add(cbOrObject);
this.events.set(type, cbs);
return () => this.removeEventListener(type, cbOrObject);
}

public removeEventListener(type: string, cbOrObject: TEventedComponentListener) {
const cbs = this.events.get(type);
if (cbs) {
cbs.delete(cbOrObject);
}
}

public _fireEvent(cmp: Component, event: Event) {
const handlers = listeners.get(cmp)?.get?.(event.type);

handlers?.forEach((cb) => {
if (typeof cb === "function") {
cb(event);

Check warning on line 56 in src/components/canvas/EventedComponent/EventedComponent.ts

View workflow job for this annotation

GitHub Actions / Verify Files

Expected return with your callback function
} else if (cb instanceof Component && "handleEvent" in cb && typeof cb.handleEvent === "function") {
cb.handleEvent?.(event);
}
});
}

public dispatchEvent(event: Event): boolean {
const bubbles = event.bubbles || false;

if (bubbles) {
return this._dipping(this, event);
} else if (this._hasListener(this, event.type)) {
this._fireEvent(this, event);
return false;
}
return false;
}

public _dipping(startParent: Component, event: Event) {
let stopPropagation = false;
let parent: Component = startParent;
event.stopPropagation = () => {
stopPropagation = true;
};

do {
this._fireEvent(parent, event);
if (stopPropagation) {
return false;
}
parent = parent.getParent() as Component;
} while (parent);

return true;
}

public _hasListener(comp: EventedComponent, type: string) {
return listeners.get(comp)?.has?.(type);
}
}
69 changes: 69 additions & 0 deletions src/components/canvas/GraphComponent/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Signal } from "@preact/signals-core";

import { Graph } from "../../../graph";
import { Component } from "../../../lib";
import { TComponentContext, TComponentProps, TComponentState } from "../../../lib/Component";
import { HitBox, HitBoxData } from "../../../services/HitTest";
import { EventedComponent } from "../EventedComponent/EventedComponent";
import { TGraphLayerContext } from "../layers/graphLayer/GraphLayer";

export type GraphComponentContext = TComponentContext &
TGraphLayerContext & {
graph: Graph;
};

export class GraphComponent<
Props extends TComponentProps = TComponentProps,
State extends TComponentState = TComponentState,
Context extends GraphComponentContext = GraphComponentContext,
> extends EventedComponent<Props, State, Context> {
public hitBox: HitBox;

private unsubscribe: (() => void)[] = [];

constructor(props: Props, parent: Component) {
super(props, parent);
this.hitBox = new HitBox(this, this.context.graph.hitTest);
}

protected subscribeSignal<T>(signal: Signal<T>, cb: (v: T) => void) {
this.unsubscribe.push(signal.subscribe(cb));
}

protected unmount() {
super.unmount();
this.unsubscribe.forEach((cb) => cb());
this.destroyHitBox();
}

public setHitBox(minX: number, minY: number, maxX: number, maxY: number, force?: boolean) {
this.hitBox.update(minX, minY, maxX, maxY, force);
}

protected willIterate(): void {
super.willIterate();
if (!this.firstIterate) {
this.shouldRender = this.isVisible();
}
}

protected isVisible() {
return this.context.camera.isRectVisible(...this.getHitBox());
}

public getHitBox() {
return this.hitBox.getRect();
}

public removeHitBox() {
this.hitBox.remove();
}

public destroyHitBox() {
this.hitBox.destroy();
}

public onHitBox(_: HitBoxData) {
return this.isIterated();
}
}
56 changes: 17 additions & 39 deletions src/components/canvas/anchors/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { EventedComponent } from "../../../mixins/withEvents";
import { withHitTest } from "../../../mixins/withHitTest";
import { ECameraScaleLevel } from "../../../services/camera/CameraService";
import { frameDebouncer } from "../../../services/optimizations/frameDebouncer";
import { AnchorState, EAnchorType } from "../../../store/anchor/Anchor";
import { TBlockId } from "../../../store/block/Block";
import { selectBlockAnchor } from "../../../store/block/selectors";
import { TPoint } from "../../../utils/types/shapes";
import { GraphComponent } from "../GraphComponent";
import { GraphLayer, TGraphLayerContext } from "../layers/graphLayer/GraphLayer";

export type TAnchor = {
Expand All @@ -28,7 +27,7 @@ type TAnchorState = {
selected: boolean;
};

export class Anchor extends withHitTest(EventedComponent) {
export class Anchor extends GraphComponent<TAnchorProps, TAnchorState> {
public readonly cursor = "pointer";

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

private hitBoxHash: string;

private debouncedSetHitBox: (...args: any[]) => void;

protected readonly unsubscribe: (() => void)[] = [];
private debouncedSetHitBox = frameDebouncer.add(
() => {
const { x, y } = this.props.getPosition(this.props);
this.setHitBox(x - this.shift, y - this.shift, x + this.shift, y + this.shift);
},
{
delay: 4,
lightFrame: true,
}
);

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

this.connectedState = selectBlockAnchor(this.context.graph, props.blockId, props.id);

if (this.connectedState) {
this.unsubscribe = this.subscribe();
}

this.debouncedSetHitBox = frameDebouncer.add(this.bindedSetHitBox.bind(this), {
delay: 4,
lightFrame: true,
this.subscribeSignal(this.connectedState.$selected, (selected) => {
this.setState({ selected });
});

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

protected subscribe() {
return [
this.connectedState.$selected.subscribe((selected) => {
this.setState({ selected });
}),
];
}

protected unmount() {
this.unsubscribe.forEach((reactionDisposer) => reactionDisposer());

super.unmount();
}

public toggleSelected() {
this.connectedState.setSelection(!this.state.selected);
}

public willIterate() {
super.willIterate();

const { x, y, width, height } = this.hitBox.getRect();

this.shouldRender = width && height ? this.context.camera.isRectVisible(x, y, width, height) : true;
protected isVisible() {
const params = this.getHitBox();
return params ? this.context.camera.isRectVisible(...params) : true;
}

public didIterate(): void {
Expand Down Expand Up @@ -137,11 +120,6 @@ export class Anchor extends withHitTest(EventedComponent) {
}
}

public bindedSetHitBox() {
const { x, y } = this.props.getPosition(this.props);
this.setHitBox(x - this.shift, y - this.shift, x + this.shift, y + this.shift);
}

private computeRenderSize(size: number, raised: boolean) {
if (raised) {
this.setState({ size: size * 1.8 });
Expand Down
Loading

0 comments on commit fe4fb33

Please sign in to comment.