Skip to content

Commit

Permalink
New: Add state setters with options (#95)
Browse files Browse the repository at this point in the history
* New: Add state setters with options

* Chore: Remove leftover comments

* Chore: Refactor state setter logic

* Chore: Refactor state types
  • Loading branch information
AlexIchenskiy authored Mar 20, 2024
1 parent 0ee733d commit c9de1cf
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 11 deletions.
49 changes: 44 additions & 5 deletions src/models/edge.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { INodeBase, INode } from './node';
import { GraphObjectState } from './state';
import { GraphObjectState, IGraphObjectStateOptions, IGraphObjectStateParameters } from './state';
import { Color, IPosition, ICircle, getDistanceToLine } from '../common';
import { isArrayOfNumbers, isFunction } from '../utils/type.utils';
import { isArrayOfNumbers, isFunction, isNumber, isPlainObject } from '../utils/type.utils';
import { IObserver, ISubject, Subject } from '../utils/observer.utils';
import { patchProperties } from '../utils/object.utils';

Expand Down Expand Up @@ -120,7 +120,9 @@ export interface IEdge<N extends INodeBase, E extends IEdgeBase> extends ISubjec
patchStyle(style: IEdgeStyle): void;
patchStyle(callback: (edge: IEdge<N, E>) => IEdgeStyle): void;
setState(state: number): void;
setState(state: IGraphObjectStateParameters): void;
setState(callback: (edge: IEdge<N, E>) => number): void;
setState(callback: (edge: IEdge<N, E>) => IGraphObjectStateParameters): void;
}

export interface IEdgeSettings {
Expand Down Expand Up @@ -400,15 +402,52 @@ abstract class Edge<N extends INodeBase, E extends IEdgeBase> extends Subject im
}

setState(state: number): void;
setState(state: IGraphObjectStateParameters): void;
setState(callback: (edge: IEdge<N, E>) => number): void;
setState(arg: number | ((edge: IEdge<N, E>) => number)): void {
setState(callback: (edge: IEdge<N, E>) => IGraphObjectStateParameters): void;
setState(
arg:
| number
| IGraphObjectStateParameters
| ((edge: IEdge<N, E>) => number)
| ((edge: IEdge<N, E>) => IGraphObjectStateParameters),
): void {
let result: number | IGraphObjectStateParameters;

if (isFunction(arg)) {
this._state = (arg as (edge: IEdge<N, E>) => number)(this);
result = (arg as (edge: IEdge<N, E>) => number | IGraphObjectStateParameters)(this);
} else {
this._state = arg as number;
result = arg;
}

if (isNumber(result)) {
this._state = result;
} else if (isPlainObject(result)) {
const options = result.options;

this._state = this._handleState(result.state, options);

if (options) {
this.notifyListeners({
id: this.id,
type: 'edge',
options: options,
});

return;
}
}

this.notifyListeners();
}

private _handleState(state: number, options?: Partial<IGraphObjectStateOptions>): number {
if (options?.isToggle && this._state === state) {
return GraphObjectState.NONE;
} else {
return state;
}
}
}

const getEdgeType = <N extends INodeBase, E extends IEdgeBase>(data: IEdgeData<N, E>): EdgeType => {
Expand Down
22 changes: 22 additions & 0 deletions src/models/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,28 @@ export class Graph<N extends INodeBase, E extends IEdgeBase> extends Subject imp
// Arrow function is used because they inherit the context from the enclosing scope
// which is important for the callback to notify listeners as expected
private _update: IObserver = (data?: IObserverDataPayload): void => {
if (data && 'type' in data && 'options' in data && 'isSingle' in data.options) {
if (data.type === 'node' && data.options.isSingle) {
const nodes = this._nodes.getAll();

for (let i = 0; i < nodes.length; i++) {
if (nodes[i].id !== data.id) {
nodes[i].clearState();
}
}
}

if (data.type === 'edge' && data.options.isSingle) {
const edges = this._edges.getAll();

for (let i = 0; i < edges.length; i++) {
if (edges[i].id !== data.id) {
edges[i].clearState();
}
}
}
}

this.notifyListeners(data);
};

Expand Down
49 changes: 44 additions & 5 deletions src/models/node.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { IEdge, IEdgeBase } from './edge';
import { Color, IPosition, IRectangle, isPointInRectangle } from '../common';
import { ImageHandler } from '../services/images';
import { GraphObjectState } from './state';
import { GraphObjectState, IGraphObjectStateOptions, IGraphObjectStateParameters } from './state';
import { IObserver, ISubject, Subject } from '../utils/observer.utils';
import { patchProperties } from '../utils/object.utils';
import { isFunction } from '../utils/type.utils';
import { isFunction, isNumber, isPlainObject } from '../utils/type.utils';

/**
* Node baseline object with required fields
Expand Down Expand Up @@ -122,7 +122,9 @@ export interface INode<N extends INodeBase, E extends IEdgeBase> extends ISubjec
patchStyle(style: INodeStyle): void;
patchStyle(callback: (node: INode<N, E>) => INodeStyle): void;
setState(state: number): void;
setState(state: IGraphObjectStateParameters): void;
setState(callback: (node: INode<N, E>) => number): void;
setState(callback: (node: INode<N, E>) => IGraphObjectStateParameters): void;
}

// TODO: Dirty solution: Find another way to listen for global images, maybe through
Expand Down Expand Up @@ -504,17 +506,54 @@ export class Node<N extends INodeBase, E extends IEdgeBase> extends Subject impl
}

setState(state: number): void;
setState(state: IGraphObjectStateParameters): void;
setState(callback: (node: INode<N, E>) => number): void;
setState(arg: number | ((node: INode<N, E>) => number)): void {
setState(callback: (node: INode<N, E>) => IGraphObjectStateParameters): void;
setState(
arg:
| number
| IGraphObjectStateParameters
| ((node: INode<N, E>) => number)
| ((node: INode<N, E>) => IGraphObjectStateParameters),
): void {
let result: number | IGraphObjectStateParameters;

if (isFunction(arg)) {
this._state = (arg as (node: INode<N, E>) => number)(this);
result = (arg as (node: INode<N, E>) => number | IGraphObjectStateParameters)(this);
} else {
this._state = arg as number;
result = arg;
}

if (isNumber(result)) {
this._state = result;
} else if (isPlainObject(result)) {
const options = result.options;

this._state = this._handleState(result.state, options);

if (options) {
this.notifyListeners({
id: this.id,
type: 'node',
options: options,
});

return;
}
}

this.notifyListeners();
}

protected _isPointInBoundingBox(point: IPosition): boolean {
return isPointInRectangle(this.getBoundingBox(), point);
}

private _handleState(state: number, options?: Partial<IGraphObjectStateOptions>): number {
if (options?.isToggle && this._state === state) {
return GraphObjectState.NONE;
} else {
return state;
}
}
}
18 changes: 18 additions & 0 deletions src/models/state.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
import { GraphObject } from '../utils/observer.utils';

// Enum is dismissed so user can define custom additional events (numbers)
export const GraphObjectState = {
NONE: 0,
SELECTED: 1,
HOVERED: 2,
};

export interface IGraphObjectStateOptions {
isToggle: boolean;
isSingle: boolean;
}

export interface IGraphObjectStateParameters {
state: number;
options?: Partial<IGraphObjectStateOptions>;
}

export interface ISetStateDataPayload {
id: any;
type: GraphObject;
options: Partial<IGraphObjectStateOptions>;
}
5 changes: 4 additions & 1 deletion src/utils/observer.utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { INodeCoordinates, INodePosition } from '../models/node';
import { ISetStateDataPayload } from '../models/state';

export type IObserverDataPayload = INodePosition | INodeCoordinates;
export type GraphObject = 'node' | 'edge';

export type IObserverDataPayload = INodePosition | INodeCoordinates | ISetStateDataPayload;

// Using callbacks here to ensure that the Observer update is abstracted from the user
export type IObserver = (data?: IObserverDataPayload) => void;
Expand Down

0 comments on commit c9de1cf

Please sign in to comment.