History
Overview
This plugin is used to implement the Undo and Redo functions in graph editing. By recording the historical state stack of user operations, it supports backtracking or restoring operations during graph interactions. The plugin provides users with comprehensive configuration options and APIs.
Usage Scenarios
The history plugin is suitable for all scenarios involving graph editing.
Online Experience
import { createGraph } from '/demo-runtime.ts';
createGraph(
{
data: { nodes: [{ id: 'node-1' }] },
layout: { type: 'force' },
node: {
style: {
size: 60,
labelText: 'Drag Me!',
labelPlacement: 'middle',
labelFill: '#fff',
fill: '#7e3feb',
},
},
edge: { style: { stroke: '#8b9baf' } },
behaviors: ['drag-element'],
plugins: ['grid-line', { type: 'history', key: 'history' }],
},
{ width: 600, height: 300 },
(gui, graph) => {
const options = {
type: 'history',
stackSize: 0,
};
const optionFolder = gui.addFolder('History Options');
optionFolder.add(options, 'type').disable(true);
optionFolder.add(options, 'stackSize', 0, 10, 1);
optionFolder.onChange(({ property, value }) => {
graph.updatePlugin({
key: 'history',
[property]: value,
});
graph.render();
});
const apiFolder = gui.addFolder('History API');
const instance = graph.getPluginInstance('history');
apiFolder.add(instance, 'undo');
apiFolder.add(instance, 'redo');
apiFolder.add(instance, 'clear');
},
);
import { Graph } from '@antv/g6';
import GUI from 'lil-gui';
export function addPanel(renderPanel: (gui: GUI) => void) {
const gui = new GUI({ container: document.body });
gui.title('Control');
Object.assign(gui.domElement.style, { position: 'absolute', top: '0', right: '0', zIndex: '10' });
renderPanel(gui);
addEventListener('pagehide', () => gui.destroy(), { once: true });
}
export async function createGraph(
options: ConstructorParameters<typeof Graph>[0],
size: { width?: number; height?: number } = {},
renderPanel?: (gui: GUI, graph: Graph) => void,
) {
const container = document.createElement('div');
Object.assign(container.style, {
width: '100%',
maxWidth: `${size.width || 600}px`,
height: `${size.height || 400}px`,
});
document.getElementById('container')!.append(container);
const graph = new Graph({ ...size, ...options, width: container.clientWidth, container, autoResize: true });
addEventListener('pagehide', () => graph.destroy(), { once: true });
await graph.render();
if (renderPanel) addPanel((gui) => renderPanel(gui, graph));
return container;
}Basic Usage
Add this plugin in the graph configuration:
1. Quick Configuration (Static)
Declare directly using a string. This method is simple but only supports default configurations and cannot be dynamically modified after configuration:
const graph = new Graph({
// Other configurations...
plugins: ['history'],
});
2. Object Configuration (Recommended)
Configure using an object form, supporting custom parameters, and allowing dynamic updates at runtime:
const graph = new Graph({
// Other configurations...
plugins: [
{
type: 'history',
key: 'history-1',
stackSize: 10,
},
],
});
Configuration Options
| Property | Description | Type | Default Value | Required |
|---|---|---|---|---|
| afterAddCommand | Called after a command is added to the Undo/Redo queue. revert is true for undo operations and false for redo operations |
(cmd: Command, revert: boolean) => void | - | |
| beforeAddCommand | Called before a command is added to the Undo/Redo queue. If this method returns false, the command will not be added to the queue. revert is true for undo operations and false for redo operations |
(cmd: Command, revert: boolean) => boolean | void | - | |
| executeCommand | Callback function when executing a command | (cmd: Command) => void | - | |
| stackSize | Maximum length of history records to be recorded | number | 0 (unlimited) |
Parameter Type Description
Command
// Single history command
interface Command {
current: CommandData; // Current data
original: CommandData; // Original data
animation: boolean; // Whether to enable animation
}
// Single history command data
interface CommandData {
add: GraphData; // Added data
update: GraphData; // Updated data
remove: GraphData; // Removed data
}
// Graph data
interface GraphData {
nodes?: NodeData[]; // Node data
edges?: EdgeData[]; // Edge data
combos?: ComboData[]; // Combo data
}
API
The history plugin provides the following APIs for users to use as needed. For how to call plugin methods, please refer to the Plugin Overview Document
History.canRedo()
Determines whether a redo operation can be performed. If there are records in the redo stack, it returns true; otherwise, it returns false.
canRedo(): boolean;
Example:
const canRedo = historyInstance.canRedo();
if (canRedo) {
console.log('Redo operation can be performed');
} else {
console.log('Redo stack is empty, cannot redo');
}
History.canUndo()
Determines whether an undo operation can be performed. If there are records in the undo stack, it returns true; otherwise, it returns false.
canUndo(): boolean;
Example:
const canUndo = historyInstance.canUndo();
if (canUndo) {
console.log('Undo operation can be performed');
} else {
console.log('Undo stack is empty, cannot undo');
}
History.clear()
Clears the history records, including the undo and redo stacks.
clear(): void;
Example:
historyInstance.clear();
console.log('History records cleared');
History.on()
Listens to history events, allowing users to execute custom logic when specific events occur.
on(event: Loosen<HistoryEvent>, handler: (e: { cmd?: Command | null }) => void): void;
Parameter Type Description:
-
HistoryEvent
enum HistoryEvent \{ UNDO = 'undo', // When a command is undone REDO = 'redo', // When a command is redone CANCEL = 'cancel', // When a command is canceled ADD = 'add', // When a command is added to the queue CLEAR = 'clear', // When the history queue is cleared CHANGE = 'change', // When the history queue changes \} -
Command
Please refer to the previous Command type description
Example:
historyInstance.on(HistoryEvent.UNDO, () => {
console.log('Undo operation executed');
});
History.redo()
Performs a redo operation and returns the plugin instance. If the redo stack is empty, no operation is performed.
redo(): History;
Example:
historyInstance.redo();
console.log('Redo operation executed');
History.undo()
Performs an undo operation and returns the plugin instance. If the undo stack is empty, no operation is performed.
undo(): History;
Example:
historyInstance.undo();
console.log('Undo operation executed');
History.undoAndCancel()
Performs an undo operation without recording it in the history and returns the plugin instance. Note that this operation will clear the redo stack.
undoAndCancel(): History;
Example:
historyInstance.undoAndCancel();
console.log('Undo and cancel operation executed');
History Modes
This plugin supports two history modes:
Default Mode
In default mode, every time a render is triggered (for example, after updating element data, the user actively executes the graph.draw() method to trigger rendering), the plugin records the data before and after rendering and stacks it as an operation record.
Custom Mode
Scenario Description
In actual needs, a user’s graph editing operation may involve multiple renders. For example, in one editing operation, first display nodes A and B, then display the connection from A to B. This involves two renders (i.e., the user needs to perform graph.draw() twice). In this scenario, the default mode will stack two history records, which are:
- Display nodes A and B
- Display the connection from A to B
Obviously, in actual business, one operation should only require one undo.
But here, when undoing this operation, the user needs to call the undo method twice, which means two undos are required.
Scenario Support
To support such scenarios, G6 provides a batch controller (BatchController, refer to the source code), which is provided in the graph instance context.
The history plugin implements custom operation records based on this batch controller. The code example is as follows:
const graph = new Graph({
// Other configurations...
plugins: [
{
type: 'history',
key: 'history',
},
],
});
graph.context.batch.startBatch(); // Start batch operation
graph.addNodeData(...); // Display nodes A and B
graph.draw(); // First render trigger
graph.addEdgeData(...); // Display the connection from A to B
graph.draw(); // Second render trigger
graph.context.batch.endBatch(); // End batch operation
In the example:
- By calling the
startBatchmethod of the batch controller instance, the history plugin is informed that batch operations are now being performed. Before the batch operation ends, no matter how many renders are triggered, no history records should be stacked (the history plugin will record the change data for each render trigger). - After completing the last data change, call the
endBatch()method. The history plugin listens for the completion of the batch operation and stacks this batch operation as a history record.
Finally, the user only needs to perform one undo to undo.
Code Examples
Below are some common cases with corresponding code references.
Undo and Redo Button States
In actual business scenarios, you may need to customize the toolbar of the canvas, which involves the enable and disable states of the undo and redo buttons.
const canUndo = false;
const canRedo = false;
const graph = new Graph({
// Other configurations...
plugins: [
{
type: 'history',
key: 'history',
},
],
});
const historyInstance = graph.getPluginInstance('history');
historyInstance.on(HistoryEvent.CHANGE, () => {
canUndo = historyInstance.canUndo();
canRedo = historyInstance.canRedo();
});
In the example, by listening to the HistoryEvent.CHANGE event, which is triggered when the history queue changes, it is determined in real-time whether undo and redo operations can be performed.
Determine Whether a Command is Allowed to Enter the Queue
Here is a simple scenario: only the operation of removing elements is allowed to enter the history queue.
const graph = new Graph({
// Other configurations...
plugins: [
{
type: 'history',
key: 'history',
beforeAddCommand: (cmd) => {
return (
cmd.current.remove?.nodes?.length > 0 ||
cmd.current.remove?.combos?.length > 0 ||
cmd.current.remove?.edges?.length > 0
);
},
},
],
});
In the example, the configuration option beforeAddCommand is used to determine whether there are elements removed in cmd.current.remove.