feat: update rdo/undo pause and resume stacking

This commit is contained in:
yvonneyx 2023-08-23 19:52:19 +08:00
parent 99bee31f16
commit 9242fd6203
16 changed files with 354 additions and 214 deletions

View File

@ -473,7 +473,9 @@ export class ItemController {
}
const parentItem = this.itemMap[current.parentId];
if (current.parentId && parentItem?.model.data.collapsed) {
this.graph.hideItem(innerModel.id, false, false);
this.graph.executeWithoutStacking(() => {
this.graph.hideItem(innerModel.id, false);
});
}
});
updateRelates();
@ -1097,8 +1099,11 @@ export class ItemController {
const succeedIds: ID[] = [];
// hide the succeeds
graphComboTreeDfs(this.graph, [comboModel], (child) => {
if (child.id !== comboModel.id)
this.graph.hideItem(child.id, false, false);
if (child.id !== comboModel.id) {
this.graph.executeWithoutStacking(() => {
this.graph.hideItem(child.id, false);
});
}
relatedEdges = relatedEdges.concat(graphCore.getRelatedEdges(child.id));
succeedIds.push(child.id);
});
@ -1133,7 +1138,9 @@ export class ItemController {
},
});
});
this.graph.addData('edge', virtualEdges, false);
this.graph.executeWithoutStacking(() => {
this.graph.addData('edge', virtualEdges);
});
}
private expandCombo(graphCore: GraphCore, comboModel: ComboModel) {
@ -1154,7 +1161,9 @@ export class ItemController {
});
if (child.id !== comboModel.id) {
if (!graphCore.getNode(child.data.parentId).data.collapsed) {
this.graph.showItem(child.id, false, false);
this.graph.executeWithoutStacking(() => {
this.graph.showItem(child.id, false);
});
}
// re-add collapsed succeeds' virtual edges by calling collapseCombo
if (child.data._isCombo && child.data.collapsed) {
@ -1163,7 +1172,9 @@ export class ItemController {
}
});
// remove related virtual edges
this.graph.removeData('edge', uniq(relatedVirtualEdgeIds), false);
this.graph.executeWithoutStacking(() => {
this.graph.removeData('edge', uniq(relatedVirtualEdgeIds));
});
}
}

View File

@ -102,19 +102,6 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
base: 'light',
},
enableStack: true,
stackCfg: {
stackSize: 0,
/** ignore* 是全局设置,用于指示是否忽略某种类型的操作 */
ignoreStateChange: false,
ignoreAdd: false,
ignoreRemove: false,
ignoreUpdate: false,
/** ignore
* API excludes 使
*/
includes: [],
excludes: [],
},
};
constructor(spec: Specification<B, T>) {
@ -927,7 +914,6 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
* Add one or more node/edge/combo data to the graph.
* @param itemType item type
* @param model user data
* @param stack whether push this operation to stack
* @returns whether success
* @group Data
*/
@ -940,7 +926,6 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
| NodeUserModel[]
| EdgeUserModel[]
| ComboUserModel[],
stack?: boolean,
):
| NodeModel
| EdgeModel
@ -965,7 +950,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
type: itemType,
action: 'add',
models,
enableStack: this.shouldPushToStack('addData', stack),
apiName: 'addData',
changes,
});
});
@ -986,11 +971,10 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
/**
* Remove one or more node/edge/combo data from the graph.
* @param item the item to be removed
* @param stack whether push this operation to stack
* @returns whether success
* @group Data
*/
public removeData(itemType: ITEM_TYPE, ids: ID | ID[], stack?: boolean) {
public removeData(itemType: ITEM_TYPE, ids: ID | ID[]) {
const idArr = isArray(ids) ? ids : [ids];
const data = { nodes: [], edges: [], combos: [] };
const { userGraphCore, graphCore } = this.dataController;
@ -1011,7 +995,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
type: itemType,
action: 'remove',
ids,
enableStack: this.shouldPushToStack('removeData', stack),
apiName: 'removeData',
changes,
});
});
@ -1051,10 +1035,18 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
}
});
if (oldValue.x === undefined || Number.isNaN(oldValue.x))
if (
(oldValue.x === undefined || Number.isNaN(oldValue.x)) &&
!oldValue._isCombo
)
oldValue.x = 0;
if (oldValue.y === undefined || Number.isNaN(oldValue.x))
if (
(oldValue.y === undefined || Number.isNaN(oldValue.x)) &&
!oldValue._isCombo
)
oldValue.y = 0;
if (Number.isNaN(newValue.x)) delete newValue.x;
if (Number.isNaN(newValue.y)) delete newValue.y;
if (isEqual(newValue, oldValue)) return false;
@ -1070,7 +1062,6 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
* Update one or more node/edge/combo data on the graph.
* @param {ITEM_TYPE} itemType 'node' | 'edge' | 'combo'
* @param models new configurations for every node/edge/combo, which has id field to indicate the specific item
* @param {boolean} stack whether push this operation into graph's stack, true by default
* @group Data
*/
public updateData(
@ -1084,7 +1075,6 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
| Partial<EdgeUserModel>[]
| Partial<ComboUserModel>[]
>,
stack?: boolean,
):
| NodeModel
| EdgeModel
@ -1110,7 +1100,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
type: itemType,
action: 'update',
models,
enableStack: this.shouldPushToStack('updateData', stack),
apiName: 'updateData',
changes,
});
});
@ -1130,7 +1120,6 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
* Update one or more nodes' positions,
* do not update other styles which leads to better performance than updating positions by updateData.
* @param models new configurations with x and y for every node, which has id field to indicate the specific item
* @param {boolean} stack whether push this operation into graph's stack, true by default
* @group Data
*/
public updateNodePosition(
@ -1140,9 +1129,8 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[]
>,
upsertAncestors?: boolean,
stack?: boolean,
) {
return this.updatePosition('node', models, upsertAncestors, stack);
return this.updatePosition('node', models, upsertAncestors);
}
/**
@ -1150,7 +1138,6 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
* do not update other styles which leads to better performance than updating positions by updateData.
* In fact, it changes the succeed nodes positions to affect the combo's position, but not modify the combo's position directly.
* @param models new configurations with x and y for every combo, which has id field to indicate the specific item
* @param {boolean} stack whether push this operation into graph's stack, true by default
* @group Data
*/
public updateComboPosition(
@ -1160,9 +1147,8 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[]
>,
upsertAncestors?: boolean,
stack?: boolean,
) {
return this.updatePosition('combo', models, upsertAncestors, stack);
return this.updatePosition('combo', models, upsertAncestors);
}
/**
@ -1178,26 +1164,6 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
return this.pluginController.getPlugin('history') as History;
}
/**
* Determine if a given operation should be pushed onto the history stack based on various configurations.
* @param {string} apiName name of the API operation.
* @param {boolean} stack Optional. whether push this operation into graph's stack.
*/
private shouldPushToStack(apiName: string, stack?: boolean): boolean {
const { enableStack, stackCfg } = this.specification;
const { includes: defaultIncludes, excludes: defaultExcludes } =
this.defaultSpecification.stackCfg;
const { includes = defaultIncludes, excludes = defaultExcludes } = stackCfg;
if (!enableStack) return false;
if (isBoolean(stack)) return stack;
if (includes.includes(apiName)) return true;
if (excludes.includes(apiName)) return false;
let categoryKey: any = getCategoryByApiName(apiName);
if (!categoryKey) return true;
categoryKey = 'ignore' + upperFirst(categoryKey);
return !stackCfg[categoryKey];
}
private updatePosition(
type: 'node' | 'combo',
models:
@ -1206,7 +1172,6 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[]
>,
upsertAncestors?: boolean,
stack?: boolean,
) {
const modelArr = isArray(models) ? models : [models];
const { graphCore } = this.dataController;
@ -1229,7 +1194,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
action: 'updatePosition',
upsertAncestors,
models,
enableStack: this.shouldPushToStack('updatePosition', stack),
apiName: 'updatePosition',
changes,
});
});
@ -1269,7 +1234,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
* @returns
* @group Item
*/
public showItem(ids: ID | ID[], disableAnimate?: boolean, stack?: boolean) {
public showItem(ids: ID | ID[], disableAnimate?: boolean) {
const idArr = isArray(ids) ? ids : [ids];
if (isEmpty(idArr)) return;
const changes = {
@ -1287,7 +1252,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
value: true,
animate: !disableAnimate,
action: 'updateVisibility',
enableStack: this.shouldPushToStack('showItem', stack),
apiName: 'showItem',
changes,
});
}
@ -1297,7 +1262,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
* @returns
* @group Item
*/
public hideItem(ids: ID | ID[], disableAnimate?: boolean, stack?: boolean) {
public hideItem(ids: ID | ID[], disableAnimate?: boolean) {
const idArr = isArray(ids) ? ids : [ids];
if (isEmpty(idArr)) return;
const changes = {
@ -1315,7 +1280,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
value: false,
animate: !disableAnimate,
action: 'updateVisibility',
enableStack: this.shouldPushToStack('hideItem', stack),
apiName: 'hideItem',
changes,
});
}
@ -1326,7 +1291,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
* @returns
* @group Item
*/
public frontItem(ids: ID | ID[], stack?: boolean) {
public frontItem(ids: ID | ID[]) {
const idArr = isArray(ids) ? ids : [ids];
this.hooks.itemzindexchange.emit({
ids: idArr as ID[],
@ -1336,7 +1301,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
this.emit('afteritemzindexchange', {
ids: idArr,
action: 'front',
enableStack: this.shouldPushToStack('frontItem', stack),
apiName: 'frontItem',
});
}
/**
@ -1345,7 +1310,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
* @returns
* @group Item
*/
public backItem(ids: ID | ID[], stack?: boolean) {
public backItem(ids: ID | ID[]) {
const idArr = isArray(ids) ? ids : [ids];
this.hooks.itemzindexchange.emit({
ids: idArr as ID[],
@ -1355,7 +1320,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
this.emit('afteritemzindexchange', {
ids: idArr,
action: 'back',
enableStack: this.shouldPushToStack('backItem', stack),
apiName: 'backItem',
});
}
@ -1388,7 +1353,6 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
ids: ID | ID[],
states: string | string[],
value: boolean,
stack?: boolean,
) {
const idArr = isArray(ids) ? ids : [ids];
const stateArr = isArray(states) ? states : [states];
@ -1407,7 +1371,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
states,
value,
action: 'updateState',
enableStack: this.shouldPushToStack('setItemState', stack),
apiName: 'setItemState',
changes,
});
}
@ -1429,7 +1393,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
* @returns
* @group Item
*/
public clearItemState(ids: ID | ID[], states?: string[], stack?: boolean) {
public clearItemState(ids: ID | ID[], states?: string[]) {
const idArr = isArray(ids) ? ids : [ids];
const stateOptions = { ids: idArr, states, value: false };
const changes = {
@ -1446,7 +1410,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
states,
value: false,
action: 'updateState',
enableStack: this.shouldPushToStack('clearItemState', stack),
apiName: 'clearItemState',
changes,
});
}
@ -1482,15 +1446,10 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
* Add a new combo to the graph, and update the structure of the existed child in childrenIds to be the children of the new combo.
* Different from addData with combo type, this API update the succeeds' combo tree strucutres in the same time.
* @param model combo user data
* @param stack whether push this operation to stack
* @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) => {
@ -1506,7 +1465,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
type: 'combo',
action: 'add',
models: [model],
enableStack: this.shouldPushToStack('addCombo', stack),
apiName: 'addCombo',
changes,
});
});
@ -1535,38 +1494,39 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
* @param comboId combo ids
* @group Combo
*/
public collapseCombo(comboIds: ID | ID[], stack?: boolean) {
public collapseCombo(comboIds: ID | ID[]) {
const ids = isArray(comboIds) ? comboIds : [comboIds];
this.updateData(
'combo',
ids.map((id) => ({ id, data: { collapsed: true } })),
false,
);
this.executeWithoutStacking(() => {
this.updateData(
'combo',
ids.map((id) => ({ id, data: { collapsed: true } })),
);
});
this.emit('aftercollapsecombo', {
type: 'combo',
action: 'collapseCombo',
ids: comboIds,
enableStack: this.shouldPushToStack('collapseCombo', stack),
apiName: 'collapseCombo',
});
}
/**
* Expand a combo.
* @param combo combo ids
*
* @group Combo
*/
public expandCombo(comboIds: ID | ID[], stack?: boolean) {
public expandCombo(comboIds: ID | ID[]) {
const ids = isArray(comboIds) ? comboIds : [comboIds];
this.updateData(
'combo',
ids.map((id) => ({ id, data: { collapsed: false } })),
false,
);
this.executeWithoutStacking(() => {
this.updateData(
'combo',
ids.map((id) => ({ id, data: { collapsed: false } })),
);
});
this.emit('afterexpandcombo', {
type: 'combo',
action: 'expandCombo',
ids: comboIds,
enableStack: this.shouldPushToStack('expandCombo', stack),
apiName: 'expandCombo',
});
}
@ -1575,7 +1535,6 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
* do not update other styles which leads to better performance than updating positions by updateData.
* In fact, it changes the succeed nodes positions to affect the combo's position, but not modify the combo's position directly.
* @param models new configurations with x and y for every combo, which has id field to indicate the specific item
* @param {boolean} stack whether push this operation into graph's stack, true by default
* @group Combo
*/
public moveCombo(
@ -1583,7 +1542,6 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
dx: number,
dy: number,
upsertAncestors?: boolean,
stack?: boolean,
): ComboModel[] {
const idArr = isArray(ids) ? ids : [ids];
const { graphCore } = this.dataController;
@ -1606,7 +1564,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
dy,
action: 'updatePosition',
upsertAncestors,
enableStack: this.shouldPushToStack('moveCombo', stack),
apiName: 'moveCombo',
changes,
});
});
@ -1936,6 +1894,36 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
return history.push(cmd, stackType, isNew);
}
/**
* Pause stacking operation.
*/
public pauseStacking(): void {
const history = this.getHistoryPlugin();
return history.pauseStacking();
}
/**
* Resume stacking operation.
*/
public resumeStacking(): void {
const history = this.getHistoryPlugin();
return history.resumeStacking();
}
/**
* Execute a callback without allowing any stacking operations.
* @param callback
*/
public executeWithoutStacking = (callback: () => void): void => {
const history = this.getHistoryPlugin();
history.pauseStacking();
try {
callback();
} finally {
history.resumeStacking();
}
};
/**
* Retrieve the current redo stack which consists of operations that could be undone
*/

View File

@ -118,20 +118,24 @@ export default class ActivateRelations extends Behavior {
edgeIds,
);
/** 节点 */
graph.setItemState(activeNodeIds, ACTIVE_STATE, true);
graph.setItemState(inactiveNodeIds, ACTIVE_STATE, false);
/** 边 */
graph.setItemState(activeEdgeIds, ACTIVE_STATE, true);
graph.setItemState(inactiveEdgeIds, ACTIVE_STATE, false);
graph.batch(() => {
/** 节点 */
graph.setItemState(activeNodeIds, ACTIVE_STATE, true);
graph.setItemState(inactiveNodeIds, ACTIVE_STATE, false);
/** 边 */
graph.setItemState(activeEdgeIds, ACTIVE_STATE, true);
graph.setItemState(inactiveEdgeIds, ACTIVE_STATE, false);
});
this.prevNodeIds = nodeIds;
this.prevEdgeIds = edgeIds;
}
public clearActiveState(e: any) {
const { activeState: ACTIVE_STATE } = this.options;
this.graph.setItemState(this.prevNodeIds, ACTIVE_STATE, false);
this.graph.setItemState(this.prevEdgeIds, ACTIVE_STATE, false);
this.graph.batch(() => {
this.graph.setItemState(this.prevNodeIds, ACTIVE_STATE, false);
this.graph.setItemState(this.prevEdgeIds, ACTIVE_STATE, false);
});
this.prevNodeIds = [];
this.prevEdgeIds = [];
}

View File

@ -136,13 +136,13 @@ export default class ClickSelect extends Behavior {
// Select/Unselect item.
if (this.options.shouldUpdate(event)) {
this.graph.startBatch();
if (!multiple) {
// Not multiple, clear all currently selected items
this.graph.setItemState(this.selectedIds, state, false);
}
this.graph.setItemState(itemId, state, isSelectAction);
this.graph.stopBatch();
this.graph.batch(() => {
if (!multiple) {
// Not multiple, clear all currently selected items
this.graph.setItemState(this.selectedIds, state, false);
}
this.graph.setItemState(itemId, state, isSelectAction);
});
if (isSelectAction) {
this.selectedIds.push(itemId);

View File

@ -137,18 +137,20 @@ export default class DragCanvas extends Behavior {
.getAllEdgesData()
.map((edge) => edge.id)
.filter((id) => graph.getItemVisible(id) === true);
graph.hideItem(this.hiddenEdgeIds, true, false);
this.hiddenNodeIds = graph
.getAllNodesData()
.map((node) => node.id)
.filter((id) => graph.getItemVisible(id) === true);
// draw node's keyShapes on transient, and then hidden the real nodes;
this.hiddenNodeIds.forEach((id) => {
graph.drawTransient('node', id, {
onlyDrawKeyShape: true,
graph.executeWithoutStacking(() => {
graph.hideItem(this.hiddenEdgeIds, true);
this.hiddenNodeIds = graph
.getAllNodesData()
.map((node) => node.id)
.filter((id) => graph.getItemVisible(id) === true);
// draw node's keyShapes on transient, and then hidden the real nodes;
this.hiddenNodeIds.forEach((id) => {
graph.drawTransient('node', id, {
onlyDrawKeyShape: true,
});
});
graph.hideItem(this.hiddenNodeIds, true);
});
graph.hideItem(this.hiddenNodeIds, true, false);
}
}
@ -241,15 +243,17 @@ export default class DragCanvas extends Behavior {
const { graph } = this;
if (this.options.enableOptimize) {
graph.startBatch();
if (this.hiddenEdgeIds) {
graph.showItem(this.hiddenEdgeIds, true, false);
graph.showItem(this.hiddenEdgeIds, true);
}
if (this.hiddenNodeIds) {
this.hiddenNodeIds.forEach((id) => {
this.graph.drawTransient('node', id, { action: 'remove' });
});
graph.showItem(this.hiddenNodeIds, true, false);
graph.showItem(this.hiddenNodeIds, true);
}
graph.stopBatch();
}
}

View File

@ -247,16 +247,16 @@ export default class DragCombo extends Behavior {
selectedComboIds,
this.hiddenComboTreeRoots,
);
this.graph.hideItem(
this.hiddenEdges.map((edge) => edge.id),
true,
false,
);
this.graph.hideItem(
this.hiddenComboTreeRoots.map((child) => child.id),
true,
false,
);
this.graph.executeWithoutStacking(() => {
this.graph.hideItem(
this.hiddenEdges.map((edge) => edge.id),
true,
);
this.graph.hideItem(
this.hiddenComboTreeRoots.map((child) => child.id),
true,
);
});
}
// Draw transient nodes and edges.
@ -277,17 +277,19 @@ export default class DragCombo extends Behavior {
});
// Hide original edges and nodes. They will be restored when pointerup.
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,
);
this.graph.executeWithoutStacking(() => {
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,11 +425,11 @@ export default class DragCombo extends Behavior {
}
public restoreHiddenItems() {
this.graph.pauseStacking();
if (this.hiddenEdges.length) {
this.graph.showItem(
this.hiddenEdges.map((edge) => edge.id),
true,
false,
);
this.hiddenEdges = [];
}
@ -435,7 +437,6 @@ export default class DragCombo extends Behavior {
this.graph.showItem(
this.hiddenComboTreeRoots.map((model) => model.id),
true,
false,
);
this.hiddenComboTreeRoots = [];
}
@ -445,9 +446,9 @@ export default class DragCombo extends Behavior {
this.graph.showItem(
this.originPositions.map((position) => position.id),
true,
false,
);
}
this.graph.resumeStacking();
}
public onPointerUp(event: IG6GraphEvent) {
@ -514,6 +515,7 @@ export default class DragCombo extends Behavior {
public onDropCombo(event: IG6GraphEvent) {
const newParentId = event.itemId;
this.graph.startBatch();
this.originPositions.forEach(({ id }) => {
const model = this.graph.getComboData(id);
if (!model || model.id === newParentId) return;
@ -522,10 +524,12 @@ export default class DragCombo extends Behavior {
// event.stopPropagation();
this.graph.updateData('combo', { id, data: { parentId: newParentId } });
});
this.graph.stopBatch();
this.onPointerUp(event);
}
public onDropCanvas(event: IG6GraphEvent) {
this.graph.startBatch();
this.originPositions.forEach(({ id }) => {
const model = this.graph.getComboData(id);
if (!model) return;
@ -533,6 +537,7 @@ export default class DragCombo extends Behavior {
if (!parentId) return;
this.graph.updateData('combo', { id, data: { parentId: undefined } });
});
this.graph.stopBatch();
this.onPointerUp(event);
}
}

View File

@ -236,16 +236,16 @@ export default class DragNode extends Behavior {
selectedNodeIds,
this.hiddenComboTreeItems,
);
this.graph.hideItem(
this.hiddenEdges.map((edge) => edge.id),
true,
false,
);
this.graph.hideItem(
this.hiddenComboTreeItems.map((child) => child.id),
true,
false,
);
this.graph.executeWithoutStacking(() => {
this.graph.hideItem(
this.hiddenEdges.map((edge) => edge.id),
true,
);
this.graph.hideItem(
this.hiddenComboTreeItems.map((child) => child.id),
true,
);
});
}
// Draw transient nodes and edges.
@ -268,17 +268,17 @@ export default class DragNode extends Behavior {
});
// Hide original edges and nodes. They will be restored when pointerup.
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,
);
this.graph.executeWithoutStacking(() => {
this.graph.hideItem(selectedNodeIds, true);
this.graph.hideItem(
this.hiddenEdges.map((edge) => edge.id),
true,
);
this.graph.hideItem(
this.hiddenComboTreeItems.map((combo) => combo.id),
true,
);
});
} else {
this.graph.frontItem(selectedNodeIds);
}
@ -413,11 +413,11 @@ export default class DragNode extends Behavior {
}
public restoreHiddenItems() {
this.graph.pauseStacking();
if (this.hiddenEdges.length) {
this.graph.showItem(
this.hiddenEdges.map((edge) => edge.id),
true,
false,
);
this.hiddenEdges = [];
}
@ -425,7 +425,6 @@ export default class DragNode extends Behavior {
this.graph.showItem(
this.hiddenComboTreeItems.map((edge) => edge.id),
true,
false,
);
this.hiddenComboTreeItems = [];
}
@ -435,9 +434,9 @@ export default class DragNode extends Behavior {
this.graph.showItem(
this.originPositions.map((position) => position.id),
true,
false,
);
}
this.graph.resumeStacking();
}
public clearState() {

View File

@ -11,14 +11,18 @@ export class ComboCommand implements Command {
}
undo(graph: IGraph) {
this.action === 'expandCombo'
? graph.collapseCombo(this.ids, false)
: graph.expandCombo(this.ids, false);
graph.executeWithoutStacking(() => {
this.action === 'expandCombo'
? graph.collapseCombo(this.ids)
: graph.expandCombo(this.ids);
});
}
redo(graph: IGraph) {
this.action === 'collapseCombo'
? graph.collapseCombo(this.ids, false)
: graph.expandCombo(this.ids, false);
graph.executeWithoutStacking(() => {
this.action === 'collapseCombo'
? graph.collapseCombo(this.ids)
: graph.expandCombo(this.ids);
});
}
}

View File

@ -1,5 +1,5 @@
import type { IGraph } from 'types';
import { each, isEmpty } from '@antv/util';
import { deepMix, each, isEmpty, mix, upperFirst } from '@antv/util';
import { STACK_TYPE, StackCfg, StackType } from '../../../types/history';
import { Plugin as Base, IPluginBaseConfig } from '../../../types/plugin';
import CommandFactory, { Command } from './command';
@ -81,12 +81,14 @@ export default class History extends Base {
protected undoStack: HistoryStack<Command[]>;
protected redoStack: HistoryStack<Command[]>;
protected stackSize = 0;
protected stackActive = true;
protected withoutStackingCounter = 0;
protected isBatching: boolean;
protected batchCommands: Command[];
constructor(options?: HistoryConfig) {
super();
const { enableStack, stackCfg } = options;
const { enableStack, stackCfg } = deepMix(this.getDefaultCfgs(), options);
this.enableStack = enableStack;
this.cfg = stackCfg;
this.isBatching = false;
@ -95,6 +97,24 @@ export default class History extends Base {
this.initBatchCommands();
}
public getDefaultCfgs(): HistoryConfig {
return {
enableStack: true,
stackCfg: {
stackSize: 0,
stackActive: true,
includes: [],
excludes: ['updateData'],
ignoreAdd: false,
ignoreRemove: false,
ignoreUpdate: false,
ignoreStateChange: false,
ignoreLayerChange: false,
ignoreDisplayChange: false,
},
};
}
public init(graph: IGraph) {
super.init(graph);
this.clear();
@ -137,18 +157,50 @@ export default class History extends Base {
stackType: StackType = STACK_TYPE.undo,
isNew = true,
) {
if (stackType === STACK_TYPE.undo) {
if (isNew) {
// Clear the redo stack when a new action is performed to maintain state consistency
this.clearRedoStack();
if (this.stackActive) {
if (stackType === STACK_TYPE.undo) {
if (isNew) {
// Clear the redo stack when a new action is performed to maintain state consistency
this.clearRedoStack();
}
if (isEmpty(cmd)) return;
this.undoStack.push(cmd);
this.initBatchCommands();
} else {
this.redoStack.push(cmd);
}
if (isEmpty(cmd)) return;
this.undoStack.push(cmd);
this.initBatchCommands();
this.graph.emit('history:change', cmd, stackType, isNew);
} else {
this.redoStack.push(cmd);
throw new Error(
'Stacking operations are currently paused. Unable to push to the stack.',
);
}
}
/**
* Pause stacking operations.
*/
pauseStacking(): void {
this.withoutStackingCounter++;
if (this.withoutStackingCounter === 1) {
// Only disable on the first call
this.stackActive = false;
}
}
/**
* Resume stacking operations.
*/
resumeStacking(): void {
if (this.withoutStackingCounter > 0) {
this.withoutStackingCounter--;
}
if (this.withoutStackingCounter === 0) {
// Only enable when all pause requests are cleared
this.stackActive = true;
}
this.graph.emit('history:change', cmd, stackType, isNew);
}
public undo() {
@ -223,11 +275,8 @@ export default class History extends Base {
*/
public batch(callback) {
this.startBatch();
// try {
callback();
// } finally {
this.stopBatch();
// }
}
public getEvents() {
@ -242,9 +291,9 @@ export default class History extends Base {
}
private handleUpdateHistory(props) {
const { enableStack, changes } = props;
const { apiName, changes } = props;
if (changes && changes.length === 0) return;
if (enableStack) {
if (this.shouldPushToStack(apiName)) {
this.batchCommands = [
...this.batchCommands,
...CommandFactory.create(props),
@ -256,6 +305,22 @@ export default class History extends Base {
}
}
/**
* Determine if a given operation should be pushed onto the history stack based on various configurations.
* @param {string} apiName name of the API operation.
* @param {boolean} stack Optional. whether push this operation into graph's stack.
*/
private shouldPushToStack(apiName: string): boolean {
const { includes = [], excludes = [] } = this.cfg;
if (!this.enableStack || !this.stackActive) return false;
if (includes.includes(apiName)) return true;
if (excludes.includes(apiName)) return false;
let categoryKey: any = getCategoryByApiName(apiName);
if (!categoryKey) return true;
categoryKey = 'ignore' + upperFirst(categoryKey);
return !this.cfg[categoryKey];
}
public notify(graph, eventName, ...data) {
graph.emit(eventName, data);
}

View File

@ -26,15 +26,23 @@ export class ItemDataCommand implements Command {
private removeChangedData(graph) {
const ids = this.changes.map((data) => data.value.id);
graph.removeData(this.type, ids, false);
graph.executeWithoutStacking(() => {
graph.removeData(this.type, ids);
});
}
private addChangedData(graph) {
const models = this.changes.map((data) => data.value);
graph.addData(this.type, models, false);
graph.executeWithoutStacking(() => {
graph.addData(this.type, models);
});
}
private updateChangedData(graph, operationType: StackType, onlyMove = false) {
private updateChangedData(
graph: IGraph,
operationType: StackType,
onlyMove = false,
) {
let models;
if (this.type === 'combo' && !onlyMove) {
models = this.changes.map((data) => ({
@ -53,11 +61,13 @@ export class ItemDataCommand implements Command {
});
}
graph.pauseStacking();
if (onlyMove) {
graph.updatePosition(this.type, models, this.upsertAncestors, false);
graph.updatePosition(this.type, models, this.upsertAncestors);
} else {
graph.updateData(this.type, models, false);
graph.updateData(this.type, models);
}
graph.resumeStacking();
}
undo(graph: IGraph) {

View File

@ -11,14 +11,18 @@ export class LayerUpdatedCommand implements Command {
}
undo(graph: IGraph) {
this.action === 'front'
? graph.backItem(this.ids, false)
: graph.frontItem(this.ids, false);
graph.executeWithoutStacking(() => {
this.action === 'front'
? graph.backItem(this.ids)
: graph.frontItem(this.ids);
});
}
redo(graph: IGraph) {
this.action === 'front'
? graph.frontItem(this.ids, false)
: graph.backItem(this.ids, false);
graph.executeWithoutStacking(() => {
this.action === 'front'
? graph.frontItem(this.ids)
: graph.backItem(this.ids);
});
}
}

View File

@ -20,7 +20,9 @@ export class StateUpdatedCommand implements Command {
private updateItemsStates(graph, stateOptions) {
stateOptions?.map((option) => {
const { ids, states, value } = option;
graph.setItemState(ids, states, value, false);
graph.executeWithoutStacking(() => {
graph.setItemState(ids, states, value);
});
});
}

View File

@ -19,12 +19,14 @@ export class VisibilityUpdatedCommand implements Command {
this.disableAnimate = disableAnimate;
}
private toggleItemsVisible(graph, values) {
private toggleItemsVisible(graph: IGraph, values) {
graph.pauseStacking();
each(values, (value) =>
value.visible
? graph.showItem(value.ids, this.disableAnimate, false)
: graph.hideItem(value.ids, this.disableAnimate, false),
? graph.showItem(value.ids, this.disableAnimate)
: graph.hideItem(value.ids, this.disableAnimate),
);
graph.resumeStacking();
}
undo(graph: IGraph) {

View File

@ -648,7 +648,19 @@ export interface IGraph<
* @param stackType undo/redo stack
*/
pushStack: (cmd: Command[], stackType: StackType) => void;
/**
* Pause stacking operation.
*/
pauseStacking: () => void;
/**
* Resume stacking operation.
*/
resumeStacking: () => void;
/**
* Execute a callback without allowing any stacking operations.
* @param callback
*/
executeWithoutStacking: (callback: () => void) => void;
/**
* Retrieve the current redo stack which consists of operations that could be undone
*/
@ -701,6 +713,13 @@ export interface IGraph<
*/
stopBatch: () => void;
/**
* Execute a provided function within a batched context
* All operations performed inside callback will be treated as a composite operation
* more convenient way without manually invoking `startBatch` and `stopBatch`.
* @param callback The func containing operations to be batched together.
*/
batch: (callback: () => void) => void;
/**
* Execute a provided function within a batched context
* All operations performed inside callback will be treated as a composite operation

View File

@ -1,13 +1,19 @@
export type StackCfg = {
stackSize?: number;
/** Indicate whether the stack is active. If active, operations can be pushed onto the stack; otherwise, cannot. */
stackActive?: boolean;
/** Allows finer-grained control over the ignore option.
* If an API is in excludes, even if its operation type is not ignored, it will not be put on the stack
*/
excludes?: string[];
includes?: string[];
/** ignore* is a global setting that indicates whether to ignore a certain type of operation */
ignoreAdd?: boolean;
ignoreRemove?: boolean;
ignoreUpdate?: boolean;
ignoreStateChange?: boolean;
ignoreLayerChange?: boolean;
ignoreDisplayChange?: boolean;
includes?: string[];
excludes?: string[];
};
export enum STACK_TYPE {

View File

@ -172,6 +172,14 @@ export default (context) => {
type: 'circle-node',
},
},
{
id: 'node3',
data: {
x: 100,
y: 150,
type: 'circle-node',
},
},
{
id: 'node4',
data: {
@ -193,6 +201,14 @@ export default (context) => {
type: 'line-edge',
},
},
{
id: 'edge2',
source: 'node1',
target: 'node3',
data: {
type: 'line-edge',
},
},
],
// combos: [{ id: 'combo1', data: { x: 400, y: 100 } }],
};
@ -247,10 +263,11 @@ export default (context) => {
},
edge,
// enableStack: false,
stackCfg: {
stackSize: 0,
// ignoreStateChange: true,
},
// stackCfg: {
// stackSize: 0,
// stackActive: true,
// ignoreStateChange: true,
// },
});
const { undoButton, redoButton } = createOperations(graph);