Skip to content

Commit aefac78

Browse files
authored
feat: consolidate insert context/shortcut actions (#247)
This consolidates the behaviors for the insert context menu and shortcut actions to behave the same (a behavior change for the insert shorcut) and updates the code behavior to match the delete action for simplicity.
1 parent 22fb7d8 commit aefac78

File tree

4 files changed

+156
-66
lines changed

4 files changed

+156
-66
lines changed

src/actions/clipboard.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
ICopyData,
1313
} from 'blockly';
1414
import * as Constants from '../constants';
15-
import type {BlockSvg, Workspace, WorkspaceSvg} from 'blockly';
15+
import type {BlockSvg, WorkspaceSvg} from 'blockly';
1616
import {Navigation} from '../navigation';
1717

1818
const KeyCodes = blocklyUtils.KeyCodes;

src/actions/delete.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export class DeleteAction {
9090

9191
if (!this.oldContextMenuItem) return;
9292

93-
// Unregister the original item..
93+
// Unregister the original item.
9494
ContextMenuRegistry.registry.unregister(this.oldContextMenuItem.id);
9595

9696
const deleteItem: ContextMenuRegistry.RegistryItem = {

src/actions/insert.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {
8+
Connection,
9+
ContextMenuRegistry,
10+
ShortcutRegistry,
11+
comments,
12+
utils as BlocklyUtils,
13+
} from 'blockly';
14+
import * as Constants from '../constants';
15+
import type {BlockSvg, WorkspaceSvg} from 'blockly';
16+
import {Navigation} from '../navigation';
17+
18+
const KeyCodes = BlocklyUtils.KeyCodes;
19+
20+
interface Scope {
21+
block?: BlockSvg;
22+
workspace?: WorkspaceSvg;
23+
comment?: comments.RenderedWorkspaceComment;
24+
connection?: Connection;
25+
}
26+
27+
/**
28+
* Action to insert a block into the workspace.
29+
*
30+
* This action registers itself as both a keyboard shortcut and a context menu
31+
* item.
32+
*/
33+
export class InsertAction {
34+
/**
35+
* Function provided by the navigation controller to say whether editing
36+
* is allowed.
37+
*/
38+
private canCurrentlyEdit: (ws: WorkspaceSvg) => boolean;
39+
40+
/**
41+
* Registration name for the keyboard shortcut.
42+
*/
43+
private insertShortcutName = Constants.SHORTCUT_NAMES.INSERT;
44+
45+
constructor(
46+
private navigation: Navigation,
47+
canEdit: (ws: WorkspaceSvg) => boolean,
48+
) {
49+
this.canCurrentlyEdit = canEdit;
50+
}
51+
52+
/**
53+
* Install this action as both a keyboard shortcut and a context menu item.
54+
*/
55+
install() {
56+
this.registerShortcut();
57+
this.registerContextMenuAction();
58+
}
59+
60+
/**
61+
* Uninstall this action as both a keyboard shortcut and a context menu item.
62+
* Reinstall the original context menu action if possible.
63+
*/
64+
uninstall() {
65+
ContextMenuRegistry.registry.unregister('insert');
66+
ShortcutRegistry.registry.unregister(this.insertShortcutName);
67+
}
68+
69+
/**
70+
* Create and register the keyboard shortcut for this action.
71+
*/
72+
private registerShortcut() {
73+
const insertShortcut: ShortcutRegistry.KeyboardShortcut = {
74+
name: this.insertShortcutName,
75+
preconditionFn: this.insertPrecondition.bind(this),
76+
callback: this.insertCallback.bind(this),
77+
keyCodes: [KeyCodes.I],
78+
};
79+
ShortcutRegistry.registry.register(insertShortcut);
80+
}
81+
82+
/**
83+
* Register the insert block action as a context menu item on blocks.
84+
* This function mixes together the keyboard and context menu preconditions
85+
* but only calls the keyboard callback.
86+
*/
87+
private registerContextMenuAction() {
88+
const insertAboveItem: ContextMenuRegistry.RegistryItem = {
89+
displayText: (scope) => {
90+
if (scope.block?.previousConnection) {
91+
return 'Insert Block Above (I)'
92+
} else {
93+
return 'Insert Block (I)'
94+
}
95+
},
96+
preconditionFn: (scope: Scope) => {
97+
const block = scope.block ?? scope.connection?.getSourceBlock();
98+
const ws = block?.workspace as WorkspaceSvg | null;
99+
if (!ws) return 'hidden';
100+
101+
return this.insertPrecondition(ws) ? 'enabled' : 'hidden';
102+
},
103+
callback: (scope: Scope) => {
104+
let ws =
105+
scope.block?.workspace ??
106+
(scope.connection?.getSourceBlock().workspace as WorkspaceSvg);
107+
if (!ws) return false;
108+
this.insertCallback(ws);
109+
},
110+
scopeType: ContextMenuRegistry.ScopeType.BLOCK,
111+
id: 'insert',
112+
weight: 9,
113+
};
114+
115+
ContextMenuRegistry.registry.register(insertAboveItem);
116+
}
117+
118+
/**
119+
* Precondition function for inserting a block from keyboard navigation. This
120+
* precondition is shared between keyboard shortcuts and context menu items.
121+
*
122+
* @param workspace The `WorkspaceSvg` where the shortcut was
123+
* invoked.
124+
* @returns True iff `insertCallback` function should be called.
125+
*/
126+
private insertPrecondition(workspace: WorkspaceSvg): boolean {
127+
return this.canCurrentlyEdit(workspace);
128+
}
129+
130+
/**
131+
* Callback function for inserting a block from keyboard navigation. This
132+
* callback is shared between keyboard shortcuts and context menu items.
133+
*
134+
* @param workspace The `WorkspaceSvg` where the shortcut was invoked.
135+
* @returns Whether the toolbox or flyout is successfully opened.
136+
*/
137+
private insertCallback(workspace: WorkspaceSvg): boolean {
138+
if (this.navigation.getState(workspace) === Constants.STATE.WORKSPACE) {
139+
this.navigation.openToolboxOrFlyout(workspace);
140+
return true;
141+
}
142+
return false;
143+
}
144+
}

src/navigation_controller.ts

Lines changed: 10 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,6 @@ import './gesture_monkey_patch';
1515
import * as Blockly from 'blockly/core';
1616
import {
1717
ASTNode,
18-
BlockSvg,
19-
comments,
20-
Connection,
21-
ConnectionType,
22-
ContextMenuRegistry,
23-
ICopyData,
2418
ShortcutRegistry,
2519
Toolbox,
2620
utils as BlocklyUtils,
@@ -33,20 +27,14 @@ import {Announcer} from './announcer';
3327
import {LineCursor} from './line_cursor';
3428
import {ShortcutDialog} from './shortcut_dialog';
3529
import {DeleteAction} from './actions/delete';
30+
import {InsertAction} from './actions/insert';
3631
import {Clipboard} from './actions/clipboard';
3732

3833
const KeyCodes = BlocklyUtils.KeyCodes;
3934
const createSerializedKey = ShortcutRegistry.registry.createSerializedKey.bind(
4035
ShortcutRegistry.registry,
4136
);
4237

43-
interface Scope {
44-
block?: BlockSvg;
45-
workspace?: WorkspaceSvg;
46-
comment?: comments.RenderedWorkspaceComment;
47-
connection?: Connection;
48-
}
49-
5038
/**
5139
* Class for registering shortcuts for keyboard navigation.
5240
*/
@@ -55,12 +43,18 @@ export class NavigationController {
5543
announcer: Announcer = new Announcer();
5644
shortcutDialog: ShortcutDialog = new ShortcutDialog();
5745

58-
/** Context menu and keyboard action for delete. */
46+
/** Context menu and keyboard action for deletion. */
5947
deleteAction: DeleteAction = new DeleteAction(
6048
this.navigation,
6149
this.canCurrentlyEdit.bind(this),
6250
);
6351

52+
/** Context menu and keyboard action for insertion. */
53+
insertAction: InsertAction = new InsertAction(
54+
this.navigation,
55+
this.canCurrentlyEdit.bind(this),
56+
);
57+
6458
clipboard: Clipboard = new Clipboard(
6559
this.navigation,
6660
this.canCurrentlyEdit.bind(this),
@@ -400,21 +394,6 @@ export class NavigationController {
400394
keyCodes: [KeyCodes.RIGHT],
401395
},
402396

403-
/** Connect a block to a marked location. */
404-
insert: {
405-
name: Constants.SHORTCUT_NAMES.INSERT,
406-
preconditionFn: (workspace) => this.canCurrentlyEdit(workspace),
407-
callback: (workspace) => {
408-
switch (this.navigation.getState(workspace)) {
409-
case Constants.STATE.WORKSPACE:
410-
return this.navigation.connectMarkerAndCursor(workspace);
411-
default:
412-
return false;
413-
}
414-
},
415-
keyCodes: [KeyCodes.I],
416-
},
417-
418397
/**
419398
* Enter key:
420399
*
@@ -722,39 +701,6 @@ export class NavigationController {
722701
},
723702
};
724703

725-
/**
726-
* Register the action for inserting above a block.
727-
*/
728-
protected registerInsertAction() {
729-
const insertAboveAction: ContextMenuRegistry.RegistryItem = {
730-
displayText: (scope: Scope) =>
731-
scope.block?.previousConnection ? 'Insert Block Above' : 'Insert Block',
732-
preconditionFn: (scope: Scope) => {
733-
const block = scope.block ?? scope.connection?.getSourceBlock();
734-
const ws = block?.workspace as WorkspaceSvg | null;
735-
if (!ws) return 'hidden';
736-
737-
return this.canCurrentlyEdit(ws) ? 'enabled' : 'hidden';
738-
},
739-
callback: (scope: Scope) => {
740-
let ws =
741-
scope.block?.workspace ??
742-
(scope.connection?.getSourceBlock().workspace as WorkspaceSvg);
743-
if (!ws) return false;
744-
745-
if (this.navigation.getState(ws) === Constants.STATE.WORKSPACE) {
746-
this.navigation.openToolboxOrFlyout(ws);
747-
return true;
748-
}
749-
return false;
750-
},
751-
scopeType: ContextMenuRegistry.ScopeType.BLOCK,
752-
id: 'insert',
753-
weight: 9,
754-
};
755-
ContextMenuRegistry.registry.register(insertAboveAction);
756-
}
757-
758704
/**
759705
* Registers all default keyboard shortcut items for keyboard
760706
* navigation. This should be called once per instance of
@@ -765,11 +711,10 @@ export class NavigationController {
765711
ShortcutRegistry.registry.register(shortcut);
766712
}
767713
this.deleteAction.install();
714+
this.insertAction.install();
768715

769716
this.clipboard.install();
770717

771-
this.registerInsertAction();
772-
773718
// Initalise the shortcut modal with available shortcuts. Needs
774719
// to be done separately rather at construction, as many shortcuts
775720
// are not registered at that point.
@@ -785,6 +730,7 @@ export class NavigationController {
785730
}
786731

787732
this.deleteAction.uninstall();
733+
this.insertAction.uninstall();
788734
this.clipboard.uninstall();
789735

790736
this.removeShortcutHandlers();

0 commit comments

Comments
 (0)