diff --git a/src/models/edge.ts b/src/models/edge.ts index a028775..4b36145 100644 --- a/src/models/edge.ts +++ b/src/models/edge.ts @@ -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'; @@ -120,7 +120,9 @@ export interface IEdge extends ISubjec patchStyle(style: IEdgeStyle): void; patchStyle(callback: (edge: IEdge) => IEdgeStyle): void; setState(state: number): void; + setState(state: IGraphObjectStateParameters): void; setState(callback: (edge: IEdge) => number): void; + setState(callback: (edge: IEdge) => IGraphObjectStateParameters): void; } export interface IEdgeSettings { @@ -400,15 +402,52 @@ abstract class Edge extends Subject im } setState(state: number): void; + setState(state: IGraphObjectStateParameters): void; setState(callback: (edge: IEdge) => number): void; - setState(arg: number | ((edge: IEdge) => number)): void { + setState(callback: (edge: IEdge) => IGraphObjectStateParameters): void; + setState( + arg: + | number + | IGraphObjectStateParameters + | ((edge: IEdge) => number) + | ((edge: IEdge) => IGraphObjectStateParameters), + ): void { + let result: number | IGraphObjectStateParameters; + if (isFunction(arg)) { - this._state = (arg as (edge: IEdge) => number)(this); + result = (arg as (edge: IEdge) => 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): number { + if (options?.isToggle && this._state === state) { + return GraphObjectState.NONE; + } else { + return state; + } + } } const getEdgeType = (data: IEdgeData): EdgeType => { diff --git a/src/models/graph.ts b/src/models/graph.ts index 4eafc30..076971a 100644 --- a/src/models/graph.ts +++ b/src/models/graph.ts @@ -381,6 +381,28 @@ export class Graph 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); }; diff --git a/src/models/node.ts b/src/models/node.ts index 1b48741..78afeaa 100644 --- a/src/models/node.ts +++ b/src/models/node.ts @@ -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 @@ -122,7 +122,9 @@ export interface INode extends ISubjec patchStyle(style: INodeStyle): void; patchStyle(callback: (node: INode) => INodeStyle): void; setState(state: number): void; + setState(state: IGraphObjectStateParameters): void; setState(callback: (node: INode) => number): void; + setState(callback: (node: INode) => IGraphObjectStateParameters): void; } // TODO: Dirty solution: Find another way to listen for global images, maybe through @@ -504,17 +506,54 @@ export class Node extends Subject impl } setState(state: number): void; + setState(state: IGraphObjectStateParameters): void; setState(callback: (node: INode) => number): void; - setState(arg: number | ((node: INode) => number)): void { + setState(callback: (node: INode) => IGraphObjectStateParameters): void; + setState( + arg: + | number + | IGraphObjectStateParameters + | ((node: INode) => number) + | ((node: INode) => IGraphObjectStateParameters), + ): void { + let result: number | IGraphObjectStateParameters; + if (isFunction(arg)) { - this._state = (arg as (node: INode) => number)(this); + result = (arg as (node: INode) => 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): number { + if (options?.isToggle && this._state === state) { + return GraphObjectState.NONE; + } else { + return state; + } + } } diff --git a/src/models/state.ts b/src/models/state.ts index a2f6a38..6a06839 100644 --- a/src/models/state.ts +++ b/src/models/state.ts @@ -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; +} + +export interface ISetStateDataPayload { + id: any; + type: GraphObject; + options: Partial; +} diff --git a/src/utils/observer.utils.ts b/src/utils/observer.utils.ts index e1d32ac..4b9ce60 100644 --- a/src/utils/observer.utils.ts +++ b/src/utils/observer.utils.ts @@ -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;