feat: support history (visibility, state, position)

This commit is contained in:
yvonneyx 2023-08-14 19:37:34 +08:00
parent 11a9a3016a
commit 3dd8625766
24 changed files with 821 additions and 60 deletions

View File

@ -330,13 +330,15 @@ export class DataController {
const prevNodesAndCombos = userGraphCore.getAllNodes();
const prevEdges = userGraphCore.getAllEdges();
if (prevNodesAndCombos.length && nodesAndCombos.length) {
// update the parentId
nodesAndCombos.forEach((item) => {
const { parentId } = item.data;
this.graphCore.getChildren(item.id, 'combo').forEach((child) => {
userGraphCore.mergeNodeData(child.id, { parentId });
if (this.graphCore.hasTreeStructure('combo')) {
// update the parentId
nodesAndCombos.forEach((item) => {
const { parentId } = item.data;
this.graphCore.getChildren(item.id, 'combo').forEach((child) => {
userGraphCore.mergeNodeData(child.id, { parentId });
});
});
});
}
// remove the node
userGraphCore.removeNodes(nodesAndCombos.map((node) => node.id));
}

View File

@ -282,6 +282,7 @@ export class ItemController {
upsertAncestors?: boolean;
action?: 'updatePosition';
}) {
debugger;
const {
changes,
graphCore,
@ -473,7 +474,7 @@ export class ItemController {
}
const parentItem = this.itemMap[current.parentId];
if (current.parentId && parentItem?.model.data.collapsed) {
this.graph.hideItem(innerModel.id);
this.graph.hideItem(innerModel.id, false, false);
}
});
updateRelates();
@ -1097,7 +1098,8 @@ export class ItemController {
const succeedIds: ID[] = [];
// hide the succeeds
graphComboTreeDfs(this.graph, [comboModel], (child) => {
if (child.id !== comboModel.id) this.graph.hideItem(child.id);
if (child.id !== comboModel.id)
this.graph.hideItem(child.id, false, false);
relatedEdges = relatedEdges.concat(graphCore.getRelatedEdges(child.id));
succeedIds.push(child.id);
});
@ -1153,7 +1155,7 @@ export class ItemController {
});
if (child.id !== comboModel.id) {
if (!graphCore.getNode(child.data.parentId).data.collapsed) {
this.graph.showItem(child.id);
this.graph.showItem(child.id, false, false);
}
// re-add collapsed succeeds' virtual edges by calling collapseCombo
if (child.data._isCombo && child.data.collapsed) {

View File

@ -135,6 +135,18 @@ export class PluginController {
}
}
public hasPlugin(pluginKey: string): boolean {
return this.pluginMap.has(pluginKey);
}
public getPlugin(pluginKey: string): Plugin {
const { plugin } = this.pluginMap.get(pluginKey);
if (!plugin) {
throw new Error('Plugin not found for key: ' + pluginKey);
}
return plugin;
}
private addListeners = (key: string, plugin: Plugin) => {
const events = plugin.getEvents();
this.listenersMap[key] = {};

View File

@ -1,7 +1,19 @@
import EventEmitter from '@antv/event-emitter';
import { AABB, Canvas, DisplayObject, PointLike, runtime } from '@antv/g';
import { GraphChange, ID } from '@antv/graphlib';
import { isArray, isNil, isNumber, isObject, isString } from '@antv/util';
import {
each,
groupBy,
isArray,
isBoolean,
isEmpty,
isEqual,
isNil,
isNumber,
isObject,
isString,
map,
} from '@antv/util';
import {
ComboUserModel,
EdgeUserModel,
@ -33,6 +45,8 @@ import {
import { FitViewRules, GraphTransformOptions } from '../types/view';
import { changeRenderer, createCanvas } from '../util/canvas';
import { formatPadding } from '../util/shape';
import History from '../stdlib/plugin/history';
import CommandFactory from '../stdlib/plugin/history/command';
import {
DataController,
ExtensionController,
@ -44,7 +58,6 @@ import {
} from './controller';
import { PluginController } from './controller/plugin';
import Hook from './hooks';
/**
* Disable CSS parsing for better performance.
*/
@ -70,6 +83,9 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
// the tag indicates all the three canvases are all ready
private canvasReady: boolean;
private specification: Specification<B, T>;
private enableStack: boolean;
private dataController: DataController;
private interactionController: InteractionController;
private layoutController: LayoutController;
@ -84,6 +100,10 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
type: 'spec',
base: 'light',
},
enableStack: true,
stackCfg: {
ignoreStateChange: false,
},
};
constructor(spec: Specification<B, T>) {
@ -93,6 +113,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
this.initHooks();
this.initCanvas();
this.initControllers();
this.initHistory();
this.hooks.init.emit({
canvases: {
@ -220,6 +241,16 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
});
}
private initHistory() {
this.enableStack = this.specification.enableStack;
if (this.enableStack) {
const history = { type: 'history', key: 'history' };
this.specification.plugins ||= [];
this.specification.plugins.push(history);
}
}
/**
* Change the renderer at runtime.
* @param type renderer name
@ -891,7 +922,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
| NodeUserModel[]
| EdgeUserModel[]
| ComboUserModel[],
stack?: boolean,
stack = true,
):
| NodeModel
| EdgeModel
@ -911,6 +942,14 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
graphCore,
theme: specification,
});
if (this.enableStack && stack) {
const changes = event.changes;
if (!isEmpty(changes)) {
const cmd = CommandFactory.create(changes);
const history = this.getHistoryPlugin();
history.push(cmd);
}
}
this.emit('afteritemchange', { type: itemType, action: 'add', models });
});
@ -975,7 +1014,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
| Partial<EdgeUserModel>[]
| Partial<ComboUserModel>[]
>,
stack?: boolean,
stack = true,
):
| NodeModel
| EdgeModel
@ -1028,7 +1067,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[]
>,
upsertAncestors?: boolean,
stack?: boolean,
stack = true,
) {
return this.updatePosition('node', models, upsertAncestors, stack);
}
@ -1048,11 +1087,21 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[]
>,
upsertAncestors?: boolean,
stack?: boolean,
stack = true,
) {
return this.updatePosition('combo', models, upsertAncestors, stack);
}
/**
* Get history plugin instance
*/
private getHistoryPlugin(): History {
if (this.enableStack) {
return this.pluginController.getPlugin('history') as History;
}
throw new Error('History plugin is currently not configured.');
}
private updatePosition(
type: 'node' | 'combo',
models:
@ -1061,7 +1110,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[]
>,
upsertAncestors?: boolean,
stack?: boolean,
stack = true,
) {
const modelArr = isArray(models) ? models : [models];
const { graphCore } = this.dataController;
@ -1076,6 +1125,16 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
upsertAncestors,
action: 'updatePosition',
});
if (this.enableStack && stack) {
const changes = event.changes.filter(
(change) => !isEqual(change.newValue, change.oldValue),
);
if (!isEmpty(changes)) {
const cmd = CommandFactory.create(changes, 'updatePosition');
const history = this.getHistoryPlugin();
history.push(cmd);
}
}
this.emit('afteritemchange', {
type,
action: 'updatePosition',
@ -1099,19 +1158,51 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
return isArray(models) ? dataList : dataList[0];
}
private getItemPreviousVisibility(ids: ID[]) {
const objs = ids.map((id) => ({ id, visible: this.getItemVisible(id) }));
const groupedByVisible = groupBy(objs, 'visible');
const values = [];
for (const visible in groupedByVisible) {
values.push({
ids: map(groupedByVisible[visible], (item) => item.id),
visible: Boolean(visible),
});
}
console.log('values', values);
return values;
}
/**
* Show the item(s).
* @param item the item to be shown
* @returns
* @group Item
*/
public showItem(ids: ID | ID[], disableAniamte?: boolean) {
public showItem(ids: ID | ID[], disableAnimate?: boolean, stack = true) {
const idArr = isArray(ids) ? ids : [ids];
if (isEmpty(idArr)) return;
if (this.enableStack && stack) {
const changes = {
newValue: [{ ids: idArr, visible: true }],
oldValue: this.getItemPreviousVisibility(idArr),
params: { disableAnimate },
};
const cmd = CommandFactory.create(changes, 'updateVisibility');
const history = this.getHistoryPlugin();
history.push(cmd);
}
this.hooks.itemvisibilitychange.emit({
ids: idArr as ID[],
value: true,
graphCore: this.dataController.graphCore,
animate: !disableAniamte,
animate: !disableAnimate,
});
this.emit('afteritemvisibilitychange', {
ids,
value: true,
animate: !disableAnimate,
});
}
/**
@ -1120,13 +1211,31 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
* @returns
* @group Item
*/
public hideItem(ids: ID | ID[], disableAniamte?: boolean) {
public hideItem(ids: ID | ID[], disableAnimate?: boolean, stack = true) {
const idArr = isArray(ids) ? ids : [ids];
if (isEmpty(idArr)) return;
if (this.enableStack && stack) {
const changes = {
newValue: [{ ids: idArr, visible: false }],
oldValue: this.getItemPreviousVisibility(idArr),
params: { disableAnimate },
};
console.log('changes', changes);
const cmd = CommandFactory.create(changes, 'updateVisibility');
const history = this.getHistoryPlugin();
history.push(cmd);
}
this.hooks.itemvisibilitychange.emit({
ids: idArr as ID[],
value: false,
graphCore: this.dataController.graphCore,
animate: !disableAniamte,
animate: !disableAnimate,
});
this.emit('afteritemvisibilitychange', {
ids,
value: false,
animate: !disableAnimate,
});
}
@ -1158,6 +1267,52 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
graphCore: this.dataController.graphCore,
});
}
private getItemPreviousStates(
stateOptions: {
ids: ID | ID[];
states: string | string[];
value: boolean;
}[],
) {
return stateOptions
.flatMap((option) => {
const { ids, states } = option;
const idArr = Array.isArray(ids) ? ids : [ids];
const stateArr = Array.isArray(states) ? states : [states];
if (isEmpty(idArr)) return;
return idArr.flatMap((id) => {
return stateArr.map((state) => ({
ids: id,
states: state,
value: this.getItemState(id, state),
}));
});
})
.filter((option) => !isEmpty(option));
}
public setItemStates(
stateOptions: {
ids: ID | ID[];
states: string | string[];
value: boolean;
}[],
stack = true,
) {
if (this.enableStack && stack) {
const changes = {
newValue: stateOptions,
oldValue: this.getItemPreviousStates(stateOptions),
};
const cmds = CommandFactory.create(changes, 'updateState');
const history = this.getHistoryPlugin();
history.push(cmds);
}
return each(stateOptions, (option) =>
this.setItemState(option.ids, option.states, option.value, false),
);
}
/**
* Set state for the item.
* @param item the item to be set
@ -1170,9 +1325,22 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
ids: ID | ID[],
states: string | string[],
value: boolean,
stack = true,
) {
const idArr = isArray(ids) ? ids : [ids];
const stateArr = isArray(states) ? states : [states];
if (this.enableStack && stack) {
if (!isEmpty(idArr)) {
const stateOptions = [{ ids: idArr, states: stateArr, value }];
const changes = {
newValue: stateOptions,
oldValue: this.getItemPreviousStates(stateOptions),
};
const cmds = CommandFactory.create(changes, 'updateState');
const history = this.getHistoryPlugin();
history.push(cmds);
}
}
this.hooks.itemstatechange.emit({
ids: idArr as ID[],
states: stateArr as string[],
@ -1198,8 +1366,20 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
* @returns
* @group Item
*/
public clearItemState(ids: ID | ID[], states?: string[]) {
public clearItemState(ids: ID | ID[], states?: string[], stack = true) {
const idArr = isArray(ids) ? ids : [ids];
if (this.enableStack && stack) {
if (!isEmpty(idArr)) {
const stateOptions = [{ ids: idArr, states, value: false }];
const changes = {
newValue: stateOptions,
oldValue: this.getItemPreviousStates(stateOptions),
};
const cmds = CommandFactory.create(changes, 'updateState');
const history = this.getHistoryPlugin();
history.push(cmds);
}
}
this.hooks.itemstatechange.emit({
ids: idArr as ID[],
states,
@ -1243,11 +1423,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
* @returns whether success
* @group Combo
*/
public addCombo(
model: ComboUserModel,
childrenIds: ID[],
stack?: boolean,
): ComboModel {
public addCombo(model: ComboUserModel, childrenIds: ID[]): ComboModel {
const { graphCore } = this.dataController;
const { specification } = this.themeController;
graphCore.once('changed', (event) => {
@ -1324,7 +1500,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
dx: number,
dy: number,
upsertAncestors?: boolean,
stack?: boolean,
stack = true,
): ComboModel[] {
const idArr = isArray(ids) ? ids : [ids];
const { graphCore } = this.dataController;
@ -1653,6 +1829,54 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
return this.itemController.getTransient(String(id));
}
// ===== history operations =====
/**
* Restore n operations that were last n reverted on the graph.
* @param steps The number of operations to undo. Default to 1.
* @returns
*/
public undo(steps?: number) {
if (!this.pluginController.hasPlugin('history')) {
console.warn('当前没有配置 history 插件,请先配置 enableStack 为 true');
return;
}
const history = this.getHistoryPlugin();
history.undo(steps);
}
/**
* Revert recent n operation(s) performed on the graph.
* @param steps The number of operations to redo. Default to 1.
* @returns
*/
public redo(steps?: number) {
if (!this.pluginController.hasPlugin('history')) {
console.warn('当前没有配置 history 插件,请先配置 enableStack 为 true');
return;
}
const history = this.getHistoryPlugin();
history.redo(steps);
}
public canUndo() {
const history = this.getHistoryPlugin();
return history.canUndo();
}
public canRedo() {
const history = this.getHistoryPlugin();
return history.canRedo();
}
public cleanHistory(type?: 'undo' | 'redo') {
const history = this.getHistoryPlugin();
if (!type) return history.clean();
return type === 'undo'
? history.cleanUndoStack()
: history.cleanRedoStack();
}
/**
* Destroy the graph instance and remove the related canvases.
* @returns

View File

@ -138,9 +138,13 @@ export default class ClickSelect extends Behavior {
if (this.options.shouldUpdate(event)) {
if (!multiple) {
// Not multiple, clear all currently selected items
this.graph.setItemState(this.selectedIds, state, false);
this.graph.setItemStates([
{ ids: this.selectedIds, states: state, value: false },
{ ids: itemId, states: state, value: isSelectAction },
]);
} else {
this.graph.setItemState(itemId, state, isSelectAction);
}
this.graph.setItemState(itemId, state, isSelectAction);
if (isSelectAction) {
this.selectedIds.push(itemId);
} else {

View File

@ -137,7 +137,7 @@ export default class DragCanvas extends Behavior {
.getAllEdgesData()
.map((edge) => edge.id)
.filter((id) => graph.getItemVisible(id) === true);
graph.hideItem(this.hiddenEdgeIds, true);
graph.hideItem(this.hiddenEdgeIds, true, false);
this.hiddenNodeIds = graph
.getAllNodesData()
.map((node) => node.id)
@ -148,7 +148,7 @@ export default class DragCanvas extends Behavior {
onlyDrawKeyShape: true,
});
});
graph.hideItem(this.hiddenNodeIds, true);
graph.hideItem(this.hiddenNodeIds, true, false);
}
}
@ -242,13 +242,13 @@ export default class DragCanvas extends Behavior {
const { graph } = this;
if (this.options.enableOptimize) {
if (this.hiddenEdgeIds) {
graph.showItem(this.hiddenEdgeIds, true);
graph.showItem(this.hiddenEdgeIds, true, false);
}
if (this.hiddenNodeIds) {
this.hiddenNodeIds.forEach((id) => {
this.graph.drawTransient('node', id, { action: 'remove' });
});
graph.showItem(this.hiddenNodeIds, true);
graph.showItem(this.hiddenNodeIds, true, false);
}
}
}

View File

@ -250,10 +250,12 @@ export default class DragCombo extends Behavior {
this.graph.hideItem(
this.hiddenEdges.map((edge) => edge.id),
true,
false,
);
this.graph.hideItem(
this.hiddenComboTreeRoots.map((child) => child.id),
true,
false,
);
}
@ -275,14 +277,16 @@ export default class DragCombo extends Behavior {
});
// Hide original edges and nodes. They will be restored when pointerup.
this.graph.hideItem(selectedComboIds, true);
this.graph.hideItem(selectedComboIds, true, false);
this.graph.hideItem(
this.hiddenEdges.map((edge) => edge.id),
true,
false,
);
this.graph.hideItem(
this.hiddenComboTreeRoots.map((child) => child.id),
true,
false,
);
} else {
this.graph.frontItem(selectedComboIds);
@ -423,6 +427,7 @@ export default class DragCombo extends Behavior {
this.graph.showItem(
this.hiddenEdges.map((edge) => edge.id),
true,
false,
);
this.hiddenEdges = [];
}
@ -430,6 +435,7 @@ export default class DragCombo extends Behavior {
this.graph.showItem(
this.hiddenComboTreeRoots.map((model) => model.id),
true,
false,
);
this.hiddenComboTreeRoots = [];
}
@ -439,6 +445,7 @@ export default class DragCombo extends Behavior {
this.graph.showItem(
this.originPositions.map((position) => position.id),
true,
false,
);
}
}

View File

@ -239,10 +239,12 @@ export default class DragNode extends Behavior {
this.graph.hideItem(
this.hiddenEdges.map((edge) => edge.id),
true,
false,
);
this.graph.hideItem(
this.hiddenComboTreeItems.map((child) => child.id),
true,
false,
);
}
@ -266,14 +268,16 @@ export default class DragNode extends Behavior {
});
// Hide original edges and nodes. They will be restored when pointerup.
this.graph.hideItem(selectedNodeIds, true);
this.graph.hideItem(selectedNodeIds, true, false);
this.graph.hideItem(
this.hiddenEdges.map((edge) => edge.id),
true,
false,
);
this.graph.hideItem(
this.hiddenComboTreeItems.map((combo) => combo.id),
true,
false,
);
} else {
this.graph.frontItem(selectedNodeIds);
@ -413,6 +417,7 @@ export default class DragNode extends Behavior {
this.graph.showItem(
this.hiddenEdges.map((edge) => edge.id),
true,
false,
);
this.hiddenEdges = [];
}
@ -420,6 +425,7 @@ export default class DragNode extends Behavior {
this.graph.showItem(
this.hiddenComboTreeItems.map((edge) => edge.id),
true,
false,
);
this.hiddenComboTreeItems = [];
}
@ -429,6 +435,7 @@ export default class DragNode extends Behavior {
this.graph.showItem(
this.originPositions.map((position) => position.id),
true,
false,
);
}
}

View File

@ -119,7 +119,7 @@ export default class ZoomCanvas extends Behavior {
.getAllEdgesData()
.map((edge) => edge.id)
.filter((id) => graph.getItemVisible(id) === true);
graph.hideItem(this.hiddenEdgeIds);
graph.hideItem(this.hiddenEdgeIds, false, false);
this.hiddenNodeIds = graph
.getAllNodesData()
.map((node) => node.id)
@ -130,7 +130,7 @@ export default class ZoomCanvas extends Behavior {
onlyDrawKeyShape: true,
});
});
graph.hideItem(this.hiddenNodeIds);
graph.hideItem(this.hiddenNodeIds, false, false);
}
}
@ -141,13 +141,13 @@ export default class ZoomCanvas extends Behavior {
if (enableOptimize) {
// restore hidden items
if (hiddenEdgeIds) {
graph.showItem(hiddenEdgeIds);
graph.showItem(hiddenEdgeIds, false, false);
}
if (hiddenNodeIds) {
hiddenNodeIds.forEach((id) => {
graph.drawTransient('node', id, { action: 'remove' });
});
graph.showItem(hiddenNodeIds);
graph.showItem(hiddenNodeIds, false, false);
}
}
this.hiddenEdgeIds = [];

View File

@ -23,6 +23,7 @@ import Legend from './plugin/legend';
import Grid from './plugin/grid';
import Tooltip from './plugin/tooltip';
import Menu from './plugin/menu';
import History from './plugin/history';
import ZoomCanvas from './behavior/zoom-canvas';
import ZoomCanvas3D from './behavior/zoom-canvas-3d';
import RotateCanvas3D from './behavior/rotate-canvas-3d';
@ -72,6 +73,7 @@ const stdLib = {
grid: Grid,
tooltip: Tooltip,
menu: Menu,
history: History,
},
nodes: {
'circle-node': CircleNode,

View File

@ -0,0 +1,49 @@
import { groupBy } from '@antv/util';
import { IGraph } from '../../../types';
import { ItemAddedCommand } from './item-added-command';
import { StateUpdatedCommand } from './state-updated-command';
import { PositionUpdatedCommand } from './position-updated-command';
import { VisibilityUpdatedCommand } from './visibility-updated-command';
export interface Command {
redo: (graph: IGraph) => void;
undo: (graph: IGraph) => void;
}
export default class CommandFactory {
static create(
options,
action?: 'updatePosition' | 'updateState' | 'updateVisibility',
) {
const onlyMove = action === 'updatePosition';
const groupedByType = groupBy(options, 'type');
const commands = [];
for (const type in groupedByType) {
switch (type) {
case 'NodeDataUpdated':
if (onlyMove) {
commands.push(new PositionUpdatedCommand(groupedByType[type]));
break;
}
case 'NodeAdded': {
commands.push(new ItemAddedCommand('node', groupedByType[type]));
break;
}
default:
break;
}
}
if (action === 'updateState') {
commands.push(new StateUpdatedCommand(options));
}
if (action === 'updateVisibility') {
commands.push(new VisibilityUpdatedCommand(options));
}
return commands;
}
}

View File

@ -0,0 +1,91 @@
import type { IGraph } from 'types';
import { each, mix } from '@antv/util';
import { Plugin as Base, IPluginBaseConfig } from '../../../types/plugin';
import { Command } from './command';
export interface HistoryConfig extends IPluginBaseConfig {
/** Default to true */
enableStack?: boolean;
/** Default to 0 stands no limit */
stackSize?: number;
}
export default class History extends Base {
public readonly cfg: Partial<HistoryConfig>;
protected undoStack: Command[][] = []; // support batch
protected redoStack: Command[][] = [];
protected stackSize = 0;
constructor(options?: HistoryConfig) {
super();
this.cfg = mix({}, this.getDefaultCfgs(), options.cfg);
}
public getDefaultCfgs(): HistoryConfig {
return {
enableStack: true,
stackSize: 0, // 0: not limit
};
}
public init(graph: IGraph) {
super.init(graph);
this.clean();
}
public clean() {
this.cleanUndoStack();
this.cleanRedoStack();
return this;
}
public cleanUndoStack() {
this.undoStack = [];
}
public cleanRedoStack() {
this.redoStack = [];
}
public isEnable() {
return this.cfg.enableStack;
}
public push(cmd: Command[]) {
// Clear the redo stack when a new action is performed to maintain state consistency
this.cleanRedoStack();
this.undoStack.push(cmd);
}
public undo(steps = 1) {
if (this.isEnable()) {
const cmds = this.undoStack.pop();
if (cmds) {
this.redoStack.push(cmds);
each(cmds, (cmd) => cmd.undo(this.graph));
}
}
return this;
}
public redo(steps = 1) {
if (this.isEnable()) {
const cmds = this.redoStack.pop();
if (cmds) {
this.undoStack.push(cmds);
for (let i = cmds.length - 1; i >= 0; i--) {
cmds[i].redo(this.graph);
}
}
}
return this;
}
public canUndo() {
return this.isEnable() && this.undoStack.length > 0;
}
public canRedo() {
return this.isEnable() && this.redoStack.length > 0;
}
}

View File

@ -0,0 +1,24 @@
import type { ITEM_TYPE } from '../../../types/item';
import type { IGraph } from '../../../types';
import type { GroupedChanges } from '../../../util/event';
import { Command } from './command';
export class ItemAddedCommand implements Command {
private diffData: GroupedChanges['NodeAdded'];
private type: ITEM_TYPE;
constructor(type, options) {
this.type = type;
this.diffData = options;
}
undo(graph: IGraph) {
const ids = this.diffData.map((data) => data.value.id);
graph.removeData(this.type, ids, false);
}
redo(graph: IGraph) {
const models = this.diffData.map((data) => data.value);
graph.addData(this.type, models, false);
}
}

View File

@ -0,0 +1,15 @@
import { GroupedChanges } from 'util/event';
import type { IGraph } from '../../../types';
import { Command } from './command';
export class ItemDataUpdatedCommand implements Command {
private diffData: GroupedChanges['NodeDataUpdated' | 'EdgeDataUpdated'];
constructor(options) {
this.diffData = options;
}
undo(graph: IGraph) {}
redo(graph: IGraph) {}
}

View File

@ -0,0 +1,27 @@
import { GroupedChanges } from 'util/event';
import type { IGraph } from '../../../types';
import { Command } from './command';
export class PositionUpdatedCommand implements Command {
private diffData: GroupedChanges['NodeDataUpdated'];
constructor(options) {
this.diffData = options;
}
undo(graph: IGraph) {
const models = this.diffData.map((data) => ({
id: data.id,
data: data.oldValue,
}));
graph.updatePosition('node', models, false, false);
}
redo(graph: IGraph) {
const models = this.diffData.map((data) => ({
id: data.id,
data: data.newValue,
}));
graph.updatePosition('node', models, false, false);
}
}

View File

@ -0,0 +1,27 @@
import type { ID, IGraph } from '../../../types';
import { Command } from './command';
interface StateOption {
ids: ID | ID[];
states: string | string[];
value: boolean;
}
export class StateUpdatedCommand implements Command {
private diffState: {
newValue: StateOption[];
oldValue: StateOption[];
};
constructor(options) {
this.diffState = options;
}
undo(graph: IGraph) {
graph.setItemStates(this.diffState.oldValue, false);
}
redo(graph: IGraph) {
graph.setItemStates(this.diffState.newValue, false);
}
}

View File

@ -0,0 +1,48 @@
import { each } from '@antv/util';
import type { ID, IGraph } from '../../../types';
import { Command } from './command';
interface Option {
ids: ID[];
visible: boolean;
}
export class VisibilityUpdatedCommand implements Command {
private diffState: {
newValue: Option[];
oldValue: Option[];
params: {
disableAnimate?: boolean;
};
};
constructor(options) {
this.diffState = options;
}
undo(graph: IGraph) {
const {
oldValue,
params: { disableAnimate },
} = this.diffState;
each(oldValue, (value) =>
value.visible
? graph.showItem(value.ids, disableAnimate, false)
: graph.hideItem(value.ids, disableAnimate, false),
);
}
redo(graph: IGraph) {
const {
newValue,
params: { disableAnimate },
} = this.diffState;
each(newValue, (value) =>
value.visible
? graph.showItem(value.ids, disableAnimate, false)
: graph.hideItem(value.ids, disableAnimate, false),
);
}
}

View File

@ -444,14 +444,14 @@ export interface IGraph<
* @returns
* @group Data
*/
showItem: (ids: ID | ID[], disableAniamte?: boolean) => void;
showItem: (ids: ID | ID[], disableAnimate?: boolean, stack?: boolean) => void;
/**
* Hide the item(s).
* @param ids the item id(s) to be hidden
* @returns
* @group Item
*/
hideItem: (ids: ID | ID[], disableAniamte?: boolean) => void;
hideItem: (ids: ID | ID[], disableAnimate?: boolean, stack?: boolean) => void;
/**
* Make the item(s) to the front.
* @param ids the item id(s) to front
@ -466,6 +466,11 @@ export interface IGraph<
* @group Item
*/
backItem: (ids: ID | ID[]) => void;
setItemStates: (
options: { ids: ID | ID[]; states: string | string[]; value: boolean }[],
stack?: boolean,
) => void;
/**
* Set state for the item(s).
* @param ids the id(s) for the item(s) to be set
@ -474,7 +479,12 @@ export interface IGraph<
* @returns
* @group Item
*/
setItemState: (ids: ID | ID[], state: string, value: boolean) => void;
setItemState: (
ids: ID | ID[],
state: string,
value: boolean,
stack?: boolean,
) => void;
/**
* Get the state value for an item.
* @param id the id for the item
@ -490,7 +500,7 @@ export interface IGraph<
* @returns
* @group Item
*/
clearItemState: (ids: ID | ID[], states?: string[]) => void;
clearItemState: (ids: ID | ID[], states?: string[], stack?: boolean) => void;
/**
* Get the rendering bbox for a node / edge / combo, or the graph (when the id is not assigned).
@ -627,4 +637,18 @@ export interface IGraph<
type: string;
[cfgName: string]: unknown;
}) => void;
/**
* Revert the last n operation(s) on the graph.
* @param {number} steps The number of steps to undo. Default to 1.
* @returns
*/
undo: (steps?: number) => void;
/**
* Restore the operation that was last n reverted on the graph.
* @param {number} steps The number of steps to redo. Default to 1.
* @returns
*/
redo: (steps?: number) => void;
}

View File

@ -117,4 +117,6 @@ export interface Specification<
/** theme */
theme?: ThemeOptionsOf<T>;
enableStack?: boolean;
}

View File

@ -74,7 +74,7 @@ export const getContextMenuEventProps = (
};
};
type GroupedChanges = {
export type GroupedChanges = {
NodeRemoved: NodeRemoved<NodeModelData>[];
EdgeRemoved: EdgeRemoved<EdgeModelData>[];
NodeAdded: NodeAdded<NodeModelData>[];
@ -121,19 +121,20 @@ export const getGroupedChanges = (
} else if (changeType === 'TreeStructureChanged') {
groupedChanges[changeType].push(change);
return;
} else {
const { id: oid } = change.value;
if (!graphCore.hasNode(oid) && !graphCore.hasEdge(oid)) {
const nid = Number(oid);
if ((!isNaN(nid) && graphCore.hasNode(nid)) || graphCore.hasEdge(nid)) {
groupedChanges[changeType].push({
...change,
value: { ...change.value, id: nid },
});
}
return;
}
}
// else {
// const { id: oid } = change.value;
// if (!graphCore.hasNode(oid) && !graphCore.hasEdge(oid)) {
// const nid = Number(oid);
// if ((!isNaN(nid) && graphCore.hasNode(nid)) || graphCore.hasEdge(nid)) {
// groupedChanges[changeType].push({
// ...change,
// value: { ...change.value, id: nid },
// });
// }
// return;
// }
// }
groupedChanges[changeType].push(change);
});
return groupedChanges;

View File

@ -29,6 +29,7 @@ import cubic_edge from './item/edge/cubic-edge';
import cubic_horizon_edge from './item/edge/cubic-horizon-edge';
import cubic_vertical_edge from './item/edge/cubic-vertical-edge';
import fisheye from './plugins/fisheye';
import history from './plugins/history';
import tooltip from './demo/tooltip';
import comboBasic from './combo/combo-basic';
import animations_node_build_in from './animations/node-build-in';
@ -65,6 +66,7 @@ export {
cubic_horizon_edge,
cubic_vertical_edge,
fisheye,
history,
tooltip,
comboBasic,
animations_node_build_in,

View File

@ -0,0 +1,191 @@
import G6 from '../../../src/index';
import { height, width } from '../../datasets/const';
const createOperationContainer = (container: HTMLElement) => {
const operationContainer = document.createElement('div');
operationContainer.id = 'operation-bar';
operationContainer.style.width = '100%';
operationContainer.style.height = '50px';
operationContainer.style.backgroundColor = '#eee';
container.appendChild(operationContainer);
};
const createOperations = (graph): any => {
const parentEle = document.getElementById('operation-bar');
if (!parentEle) return;
// undo/redo
const undoButton = document.createElement('button');
undoButton.innerText = 'undo';
undoButton.addEventListener('click', () => {
graph.undo();
});
const redoButton = document.createElement('button');
redoButton.innerText = 'redo';
redoButton.addEventListener('click', () => {
graph.redo();
});
undoButton.disabled = graph.canUndo();
redoButton.disabled = graph.canRedo();
parentEle.appendChild(undoButton);
parentEle.appendChild(redoButton);
// clear item's state
const clearStateButton = document.createElement('button');
clearStateButton.innerText = 'clear nodes selected state';
clearStateButton.addEventListener('click', () => {
graph.clearItemState([1, 2], 'selected');
});
parentEle.appendChild(clearStateButton);
// add a new node on the map
const addNodeButton = document.createElement('button');
addNodeButton.innerText = 'add a node';
addNodeButton.addEventListener('click', () => {
graph.addData('node', {
id: 'node3',
data: {
x: 300,
y: 100,
},
});
});
parentEle.appendChild(addNodeButton);
// show/hide node
const visibilityButton = document.createElement('button');
visibilityButton.innerText = 'show/hide node';
visibilityButton.addEventListener('click', () => {
const visible = graph.getItemVisible(4);
if (visible) {
graph.hideItem(4);
} else {
graph.showItem(4);
}
});
parentEle.appendChild(visibilityButton);
return { undoButton, redoButton };
};
export default (context) => {
const { container } = context;
// 1.create operation container
createOperationContainer(container!);
const data = {
nodes: [
{
id: 1,
data: {
x: 100,
y: 100,
type: 'circle-node',
},
},
{
id: 2,
data: {
x: 200,
y: 100,
type: 'circle-node',
},
},
{
id: 4,
data: {
x: 200,
y: 200,
type: 'circle-node',
},
},
],
edges: [
{
id: 'edge1',
source: 1,
target: 2,
data: {
type: 'quadratic-edge',
},
},
],
};
const edge: (data: any) => any = (edgeInnerModel: any) => {
const { id, data } = edgeInnerModel;
return {
id,
data: {
...data,
keyShape: {
controlPoints: [150, 100],
// curvePosition: 0.5,
curveOffset: [0, 20],
stroke: 'blue',
},
// iconShape: {
// // img: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg',
// text: 'label',
// fill: 'blue'
// },
labelShape: {
text: 'label',
position: 'middle',
fill: 'blue',
},
labelBackgroundShape: {
fill: 'white',
},
},
};
};
const graph = new G6.Graph({
container,
width,
height,
data,
type: 'graph',
modes: {
default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'],
},
node: (nodeInnerModel: any) => {
const { id, data } = nodeInnerModel;
return {
id,
data: {
...data,
keyShape: {
r: 16,
},
},
};
},
edge,
});
const { undoButton, redoButton } = createOperations(graph);
graph.on('afteritemchange', () => {
undoButton.disabled = !graph.canUndo();
redoButton.disabled = !graph.canRedo();
});
graph.on('afteritemstatechange', () => {
undoButton.disabled = !graph.canUndo();
redoButton.disabled = !graph.canRedo();
});
graph.on('afteritemvisibilitychange', () => {
debugger;
undoButton.disabled = !graph.canUndo();
redoButton.disabled = !graph.canRedo();
});
return graph;
};

View File

@ -241,7 +241,7 @@ const showRoute = (nodeData) => {
rangeData.nodes.forEach(node => {
const showIdx = showRangeIds.indexOf(+node.id);
if (showIdx > -1) {
graph.showItem(node.id);
graph.showItem(node.id, false, false);
rangeLayoutNodes.push(node);
}
else graph.hideItem(node.id);
@ -299,4 +299,4 @@ if (typeof window !== 'undefined')
if (!graph || graph.get('destroyed')) return;
if (!container || !container.scrollWidth || !container.scrollHeight) return;
graph.changeSize(container.scrollWidth, container.scrollHeight);
};
};

View File

@ -982,10 +982,10 @@ const hideItems = (graph) => {
const showItems = (graph) => {
graph.getNodes().forEach((node) => {
if (!node.isVisible()) graph.showItem(node);
if (!node.isVisible()) graph.showItem(node, false, false);
});
graph.getEdges().forEach((edge) => {
if (!edge.isVisible()) edge.showItem(edge);
if (!edge.isVisible()) edge.showItem(edge,false, false);
});
hiddenItemIds = [];
};