Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 37 additions & 7 deletions src/models/interaction.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
import { hoverEdge, hoverNode, selectEdge, selectNode, unhoverAll, unselectAll } from '../utils/graph.utils';
import {
hoverEdge,
hoverNode,
ISelectionOptions,
selectEdge,
selectNode,
unhoverAll,
unselectAll,
unselectEdge,
unselectNode,
} from '../utils/graph.utils';
import { IEdgeBase } from './edge';
import { IGraph } from './graph';
import { INodeBase } from './node';

export interface IGraphInteraction {
selectNodeById(id: any): boolean;
selectEdgeById(id: any): boolean;
selectNodeById(id: any, options?: ISelectionOptions): boolean;
selectEdgeById(id: any, options?: ISelectionOptions): boolean;
unselectNodeById(id: any, options?: ISelectionOptions): boolean;
unselectEdgeById(id: any, options?: ISelectionOptions): boolean;
unselectAll(): number;
hoverNodeById(id: any): boolean;
hoverEdgeById(id: any): boolean;
Expand All @@ -19,21 +31,39 @@ export class GraphInteraction<N extends INodeBase, E extends IEdgeBase> implemen
this._graph = graph;
}

selectNodeById(id: any): boolean {
selectNodeById(id: any, options?: ISelectionOptions): boolean {
const node = this._graph.getNodeById(id);
if (!node) {
return false;
}
selectNode(node);
selectNode(node, options);
return true;
}

selectEdgeById(id: any): boolean {
selectEdgeById(id: any, options?: ISelectionOptions): boolean {
const edge = this._graph.getEdgeById(id);
if (!edge) {
return false;
}
selectEdge(edge);
selectEdge(edge, options);
return true;
}

unselectNodeById(id: any, options?: ISelectionOptions): boolean {
const node = this._graph.getNodeById(id);
if (!node) {
return false;
}
unselectNode(node, options);
return true;
}

unselectEdgeById(id: any, options?: ISelectionOptions): boolean {
const edge = this._graph.getEdgeById(id);
if (!edge) {
return false;
}
unselectEdge(edge, options);
return true;
}

Expand Down
58 changes: 48 additions & 10 deletions src/models/strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,42 @@ import { INode, INodeBase } from './node';
import { IEdge, IEdgeBase } from './edge';
import { IGraph } from './graph';
import { IPosition } from '../common';
import { hoverOnlyNode, selectOnlyEdge, selectOnlyNode, unhoverAll, unselectAll } from '../utils/graph.utils';
import {
hoverOnlyNode,
selectOnlyEdge,
selectOnlyNode,
toggleEdgeSelection,
toggleNodeSelection,
unhoverAll,
unselectAll,
} from '../utils/graph.utils';

export interface IEventStrategySettings {
isDefaultSelectEnabled: boolean;
isDefaultHoverEnabled: boolean;
isDefaultMultiSelectEnabled: boolean;
isDefaultSelectCascadeEnabled: boolean;
}

export interface IEventStrategyResponse<N extends INodeBase, E extends IEdgeBase> {
isStateChanged: boolean;
changedSubject?: INode<N, E> | IEdge<N, E>;
}

export interface IEventStrategyClickOptions {
isAppend?: boolean;
}

export interface IEventStrategy<N extends INodeBase, E extends IEdgeBase> {
isSelectEnabled: boolean;
isHoverEnabled: boolean;
onMouseClick: (graph: IGraph<N, E>, point: IPosition) => IEventStrategyResponse<N, E>;
isMultiSelectEnabled: boolean;
isSelectCascadeEnabled: boolean;
onMouseClick: (
graph: IGraph<N, E>,
point: IPosition,
options?: IEventStrategyClickOptions,
) => IEventStrategyResponse<N, E>;
onMouseMove: (graph: IGraph<N, E>, point: IPosition) => IEventStrategyResponse<N, E>;
onMouseRightClick: (graph: IGraph<N, E>, point: IPosition) => IEventStrategyResponse<N, E>;
onMouseDoubleClick: (graph: IGraph<N, E>, point: IPosition) => IEventStrategyResponse<N, E>;
Expand All @@ -27,17 +47,31 @@ export class DefaultEventStrategy<N extends INodeBase, E extends IEdgeBase> impl
private _lastHoveredNode?: INode<N, E>;
public isSelectEnabled: boolean;
public isHoverEnabled: boolean;
public isMultiSelectEnabled: boolean;
public isSelectCascadeEnabled: boolean;

constructor(settings: IEventStrategySettings) {
this.isSelectEnabled = settings.isDefaultSelectEnabled;
this.isHoverEnabled = settings.isDefaultHoverEnabled;
this.isMultiSelectEnabled = settings.isDefaultMultiSelectEnabled;
this.isSelectCascadeEnabled = settings.isDefaultSelectCascadeEnabled;
}

onMouseClick(graph: IGraph<N, E>, point: IPosition): IEventStrategyResponse<N, E> {
onMouseClick(
graph: IGraph<N, E>,
point: IPosition,
options?: IEventStrategyClickOptions,
): IEventStrategyResponse<N, E> {
const isAppend = this.isMultiSelectEnabled && (options?.isAppend ?? false);

const node = graph.getNearestNode(point);
if (node) {
if (this.isSelectEnabled) {
selectOnlyNode(graph, node);
if (isAppend) {
toggleNodeSelection(node);
} else {
selectOnlyNode(graph, node, { cascade: this.isSelectCascadeEnabled });
}
}

return {
Expand All @@ -49,7 +83,11 @@ export class DefaultEventStrategy<N extends INodeBase, E extends IEdgeBase> impl
const edge = graph.getNearestEdge(point);
if (edge) {
if (this.isSelectEnabled) {
selectOnlyEdge(graph, edge);
if (isAppend) {
toggleEdgeSelection(edge);
} else {
selectOnlyEdge(graph, edge, { cascade: this.isSelectCascadeEnabled });
}
}

return {
Expand All @@ -58,7 +96,7 @@ export class DefaultEventStrategy<N extends INodeBase, E extends IEdgeBase> impl
};
}

if (!this.isSelectEnabled) {
if (!this.isSelectEnabled || isAppend) {
return { isStateChanged: false };
}

Expand Down Expand Up @@ -104,7 +142,7 @@ export class DefaultEventStrategy<N extends INodeBase, E extends IEdgeBase> impl
const node = graph.getNearestNode(point);
if (node) {
if (this.isSelectEnabled) {
selectOnlyNode(graph, node);
selectOnlyNode(graph, node, { cascade: this.isSelectCascadeEnabled });
}

return {
Expand All @@ -116,7 +154,7 @@ export class DefaultEventStrategy<N extends INodeBase, E extends IEdgeBase> impl
const edge = graph.getNearestEdge(point);
if (edge) {
if (this.isSelectEnabled) {
selectOnlyEdge(graph, edge);
selectOnlyEdge(graph, edge, { cascade: this.isSelectCascadeEnabled });
}

return {
Expand All @@ -139,7 +177,7 @@ export class DefaultEventStrategy<N extends INodeBase, E extends IEdgeBase> impl
const node = graph.getNearestNode(point);
if (node) {
if (this.isSelectEnabled) {
selectOnlyNode(graph, node);
selectOnlyNode(graph, node, { cascade: this.isSelectCascadeEnabled });
}

return {
Expand All @@ -151,7 +189,7 @@ export class DefaultEventStrategy<N extends INodeBase, E extends IEdgeBase> impl
const edge = graph.getNearestEdge(point);
if (edge) {
if (this.isSelectEnabled) {
selectOnlyEdge(graph, edge);
selectOnlyEdge(graph, edge, { cascade: this.isSelectCascadeEnabled });
}

return {
Expand Down
80 changes: 72 additions & 8 deletions src/utils/graph.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,86 @@ import { IFitZoomTransformOptions } from '../renderer/shared';
import { ILayoutSettings } from '../simulator';
import { IHierarchicalLayoutOptions } from '../simulator/engine/shared';

export const selectNode = <N extends INodeBase, E extends IEdgeBase>(node: INode<N, E>) => {
setNodeState(node, GraphObjectState.SELECTED, { isStateOverride: true });
export interface ISelectionOptions {
cascade?: boolean;
}

export const selectNode = <N extends INodeBase, E extends IEdgeBase>(
node: INode<N, E>,
options?: ISelectionOptions,
) => {
if (options?.cascade ?? true) {
setNodeState(node, GraphObjectState.SELECTED, { isStateOverride: true });
} else {
node.setState(GraphObjectState.SELECTED, { isNotifySkipped: true });
}
};

export const selectEdge = <N extends INodeBase, E extends IEdgeBase>(
edge: IEdge<N, E>,
options?: ISelectionOptions,
) => {
if (options?.cascade ?? true) {
setEdgeState(edge, GraphObjectState.SELECTED, { isStateOverride: true });
} else {
edge.setState(GraphObjectState.SELECTED, { isNotifySkipped: true });
}
};

export const unselectNode = <N extends INodeBase, E extends IEdgeBase>(
node: INode<N, E>,
options?: ISelectionOptions,
) => {
if (options?.cascade ?? true) {
setNodeState(node, GraphObjectState.NONE, { isStateOverride: true });
} else {
node.clearState();
}
};

export const selectEdge = <N extends INodeBase, E extends IEdgeBase>(edge: IEdge<N, E>) => {
setEdgeState(edge, GraphObjectState.SELECTED, { isStateOverride: true });
export const unselectEdge = <N extends INodeBase, E extends IEdgeBase>(
edge: IEdge<N, E>,
options?: ISelectionOptions,
) => {
if (options?.cascade ?? true) {
setEdgeState(edge, GraphObjectState.NONE, { isStateOverride: true });
} else {
edge.clearState();
}
};

export const selectOnlyNode = <N extends INodeBase, E extends IEdgeBase>(graph: IGraph<N, E>, node: INode<N, E>) => {
export const selectOnlyNode = <N extends INodeBase, E extends IEdgeBase>(
graph: IGraph<N, E>,
node: INode<N, E>,
options?: ISelectionOptions,
) => {
unselectAll(graph);
selectNode(node);
selectNode(node, options);
};

export const selectOnlyEdge = <N extends INodeBase, E extends IEdgeBase>(graph: IGraph<N, E>, edge: IEdge<N, E>) => {
export const selectOnlyEdge = <N extends INodeBase, E extends IEdgeBase>(
graph: IGraph<N, E>,
edge: IEdge<N, E>,
options?: ISelectionOptions,
) => {
unselectAll(graph);
selectEdge(edge);
selectEdge(edge, options);
};

export const toggleNodeSelection = <N extends INodeBase, E extends IEdgeBase>(node: INode<N, E>) => {
if (node.isSelected()) {
unselectNode(node, { cascade: false });
} else {
selectNode(node, { cascade: false });
}
};

export const toggleEdgeSelection = <N extends INodeBase, E extends IEdgeBase>(edge: IEdge<N, E>) => {
if (edge.isSelected()) {
unselectEdge(edge, { cascade: false });
} else {
selectEdge(edge, { cascade: false });
}
};

export const unselectAll = <N extends INodeBase, E extends IEdgeBase>(
Expand Down
18 changes: 17 additions & 1 deletion src/views/orb-map-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,17 @@ export class OrbMapView<N extends INodeBase, E extends IEdgeBase> implements IOr
strategy: {
isDefaultHoverEnabled: true,
isDefaultSelectEnabled: true,
isDefaultMultiSelectEnabled: false,
isDefaultSelectCascadeEnabled: true,
...settings?.strategy,
},
};

this._strategy = new DefaultEventStrategy<N, E>({
isDefaultSelectEnabled: this._settings.strategy.isDefaultSelectEnabled ?? false,
isDefaultHoverEnabled: this._settings.strategy.isDefaultHoverEnabled ?? false,
isDefaultMultiSelectEnabled: this._settings.strategy.isDefaultMultiSelectEnabled ?? true,
isDefaultSelectCascadeEnabled: this._settings.strategy.isDefaultSelectCascadeEnabled ?? true,
});

try {
Expand Down Expand Up @@ -201,6 +205,16 @@ export class OrbMapView<N extends INodeBase, E extends IEdgeBase> implements IOr
this._settings.strategy.isDefaultSelectEnabled = settings.strategy.isDefaultSelectEnabled;
this._strategy.isSelectEnabled = this._settings.strategy.isDefaultSelectEnabled;
}

if (isBoolean(settings.strategy.isDefaultMultiSelectEnabled)) {
this._settings.strategy.isDefaultMultiSelectEnabled = settings.strategy.isDefaultMultiSelectEnabled;
this._strategy.isMultiSelectEnabled = this._settings.strategy.isDefaultMultiSelectEnabled;
}

if (isBoolean(settings.strategy.isDefaultSelectCascadeEnabled)) {
this._settings.strategy.isDefaultSelectCascadeEnabled = settings.strategy.isDefaultSelectCascadeEnabled;
this._strategy.isSelectCascadeEnabled = this._settings.strategy.isDefaultSelectCascadeEnabled;
}
}
}

Expand Down Expand Up @@ -349,7 +363,9 @@ export class OrbMapView<N extends INodeBase, E extends IEdgeBase> implements IOr
this._renderer.render(this._graph);
}
} else if (event.type === 'click') {
const response = this._strategy.onMouseClick(this._graph, point);
const response = this._strategy.onMouseClick(this._graph, point, {
isAppend: event.originalEvent.shiftKey,
});
const subject = response.changedSubject;

if (subject) {
Expand Down
18 changes: 17 additions & 1 deletion src/views/orb-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ export class OrbView<N extends INodeBase, E extends IEdgeBase> implements IOrbVi
strategy: {
isDefaultHoverEnabled: true,
isDefaultSelectEnabled: true,
isDefaultMultiSelectEnabled: false,
isDefaultSelectCascadeEnabled: true,
...settings?.strategy,
},
interaction: {
Expand All @@ -110,6 +112,8 @@ export class OrbView<N extends INodeBase, E extends IEdgeBase> implements IOrbVi
this._strategy = new DefaultEventStrategy<N, E>({
isDefaultSelectEnabled: this._settings.strategy.isDefaultSelectEnabled ?? false,
isDefaultHoverEnabled: this._settings.strategy.isDefaultHoverEnabled ?? false,
isDefaultMultiSelectEnabled: this._settings.strategy.isDefaultMultiSelectEnabled ?? true,
isDefaultSelectCascadeEnabled: this._settings.strategy.isDefaultSelectCascadeEnabled ?? true,
});

try {
Expand Down Expand Up @@ -249,6 +253,16 @@ export class OrbView<N extends INodeBase, E extends IEdgeBase> implements IOrbVi
this._settings.strategy.isDefaultSelectEnabled = settings.strategy.isDefaultSelectEnabled;
this._strategy.isSelectEnabled = this._settings.strategy.isDefaultSelectEnabled;
}

if (isBoolean(settings.strategy.isDefaultMultiSelectEnabled)) {
this._settings.strategy.isDefaultMultiSelectEnabled = settings.strategy.isDefaultMultiSelectEnabled;
this._strategy.isMultiSelectEnabled = this._settings.strategy.isDefaultMultiSelectEnabled;
}

if (isBoolean(settings.strategy.isDefaultSelectCascadeEnabled)) {
this._settings.strategy.isDefaultSelectCascadeEnabled = settings.strategy.isDefaultSelectCascadeEnabled;
this._strategy.isSelectCascadeEnabled = this._settings.strategy.isDefaultSelectCascadeEnabled;
}
}

// Check if interaction settings are provided
Expand Down Expand Up @@ -468,7 +482,9 @@ export class OrbView<N extends INodeBase, E extends IEdgeBase> implements IOrbVi
const mousePoint = this.getCanvasMousePosition(event);
const simulationPoint = this._renderer.getSimulationPosition(mousePoint);

const response = this._strategy.onMouseClick(this._graph, simulationPoint);
const response = this._strategy.onMouseClick(this._graph, simulationPoint, {
isAppend: event.shiftKey,
});
const subject = response.changedSubject;

if (subject) {
Expand Down
Loading