Skip to content

Commit 3a5d238

Browse files
authored
fix: delegate delete behavior to core (#536)
* fix: delegate delete behavior to core * chore: correct tsdoc
1 parent 72cf087 commit 3a5d238

File tree

5 files changed

+51
-223
lines changed

5 files changed

+51
-223
lines changed

src/actions/delete.ts

Lines changed: 29 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -4,200 +4,73 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import {
8-
ContextMenuRegistry,
9-
Gesture,
10-
Msg,
11-
ShortcutRegistry,
12-
utils as BlocklyUtils,
13-
LineCursor,
14-
} from 'blockly';
7+
import {ContextMenuRegistry, Msg, ShortcutItems} from 'blockly';
158
import {getShortActionShortcut} from '../shortcut_formatting';
16-
import * as Constants from '../constants';
17-
import type {WorkspaceSvg} from 'blockly';
18-
import {Navigation} from '../navigation';
19-
20-
const KeyCodes = BlocklyUtils.KeyCodes;
219

2210
/**
2311
* Action to delete the block the cursor is currently on.
24-
* Registers itself as both a keyboard shortcut and a context menu item.
2512
*/
2613
export class DeleteAction {
2714
/**
28-
* Saved context menu item, which is re-registered when this action
29-
* is uninstalled.
30-
*/
31-
private oldContextMenuItem: ContextMenuRegistry.RegistryItem | null = null;
32-
33-
/**
34-
* Saved delete shortcut, which is re-registered when this action
35-
* is uninstalled.
15+
* Saved context menu item display text function, which is restored
16+
* when this action is uninstalled.
3617
*/
37-
private oldDeleteShortcut: ShortcutRegistry.KeyboardShortcut | null = null;
18+
private oldDisplayText:
19+
| ((scope: ContextMenuRegistry.Scope) => string | HTMLElement)
20+
| string
21+
| HTMLElement
22+
| undefined = undefined;
3823

3924
/**
40-
* Registration name for the keyboard shortcut.
25+
* Saved context menu item, which has its display text restored when
26+
* this action is uninstalled.
4127
*/
42-
private deleteShortcutName = Constants.SHORTCUT_NAMES.DELETE;
28+
private oldContextMenuItem: ContextMenuRegistry.RegistryItem | null = null;
4329

44-
constructor(private navigation: Navigation) {}
30+
constructor() {}
4531

4632
/**
4733
* Install this action as both a keyboard shortcut and a context menu item.
4834
*/
4935
install() {
50-
this.registerShortcut();
5136
this.registerContextMenuAction();
5237
}
5338

5439
/**
55-
* Uninstall this action as both a keyboard shortcut and a context menu item.
56-
* Reinstall the original context menu action if possible.
40+
* Reinstall the original context menu display text if possible.
5741
*/
5842
uninstall() {
59-
ContextMenuRegistry.registry.unregister('blockDeleteFromContextMenu');
60-
if (this.oldContextMenuItem) {
61-
ContextMenuRegistry.registry.register(this.oldContextMenuItem);
62-
}
63-
ShortcutRegistry.registry.unregister(this.deleteShortcutName);
64-
if (this.oldDeleteShortcut) {
65-
ShortcutRegistry.registry.register(this.oldDeleteShortcut);
43+
if (this.oldContextMenuItem && this.oldDisplayText) {
44+
this.oldContextMenuItem.displayText = this.oldDisplayText;
6645
}
6746
}
6847

6948
/**
70-
* Create and register the keyboard shortcut for this action.
71-
*/
72-
private registerShortcut() {
73-
this.oldDeleteShortcut = ShortcutRegistry.registry.getRegistry()['delete'];
74-
75-
if (!this.oldDeleteShortcut) return;
76-
77-
// Unregister the original shortcut.
78-
ShortcutRegistry.registry.unregister(this.oldDeleteShortcut.name);
79-
80-
const deleteShortcut: ShortcutRegistry.KeyboardShortcut = {
81-
name: this.deleteShortcutName,
82-
preconditionFn: this.deletePrecondition.bind(this),
83-
callback: this.deleteCallback.bind(this),
84-
keyCodes: [KeyCodes.DELETE, KeyCodes.BACKSPACE],
85-
allowCollision: true,
86-
};
87-
88-
ShortcutRegistry.registry.register(deleteShortcut);
89-
}
90-
91-
/**
92-
* Register the delete block action as a context menu item on blocks.
93-
* This function mixes together the keyboard and context menu preconditions
94-
* but only calls the keyboard callback.
49+
* Updates the text of the context menu delete action to include
50+
* the keyboard shortcut.
9551
*/
9652
private registerContextMenuAction() {
9753
this.oldContextMenuItem =
9854
ContextMenuRegistry.registry.getItem('blockDelete');
9955

10056
if (!this.oldContextMenuItem) return;
10157

102-
// Unregister the original item.
103-
ContextMenuRegistry.registry.unregister(this.oldContextMenuItem.id);
104-
105-
const deleteItem: ContextMenuRegistry.RegistryItem = {
106-
displayText: (scope) => {
107-
const shortcut = getShortActionShortcut(this.deleteShortcutName);
108-
if (!this.oldContextMenuItem) {
109-
return Msg['DELETE_BLOCK'].replace('%1', shortcut);
110-
}
58+
this.oldDisplayText = this.oldContextMenuItem.displayText;
11159

112-
type DisplayTextFn = (p1: ContextMenuRegistry.Scope) => string;
113-
// Use the original item's text, which is dynamic based on the number
114-
// of blocks that will be deleted.
115-
const oldDisplayText = this.oldContextMenuItem
116-
.displayText as DisplayTextFn;
117-
return oldDisplayText(scope) + ` (${shortcut})`;
118-
},
119-
preconditionFn: (scope, menuOpenEvent: Event) => {
120-
const ws = scope.block?.workspace;
60+
const displayText = (scope: ContextMenuRegistry.Scope) => {
61+
const shortcut = getShortActionShortcut(ShortcutItems.names.DELETE);
12162

122-
// Run the original precondition code, from the context menu option.
123-
// If the item would be hidden or disabled, respect it.
124-
const originalPreconditionResult =
125-
this.oldContextMenuItem?.preconditionFn?.(scope, menuOpenEvent) ??
126-
'enabled';
127-
if (!ws || originalPreconditionResult !== 'enabled') {
128-
return originalPreconditionResult;
129-
}
63+
// Use the original item's text, which is dynamic based on the number
64+
// of blocks that will be deleted.
65+
if (typeof this.oldDisplayText === 'function') {
66+
return this.oldDisplayText(scope) + ` (${shortcut})`;
67+
} else if (typeof this.oldDisplayText === 'string') {
68+
return this.oldDisplayText + ` (${shortcut})`;
69+
}
13070

131-
// Return enabled if the keyboard shortcut precondition is allowed,
132-
// and disabled if the context menu precondition is met but the keyboard
133-
// shortcut precondition is not met.
134-
return this.deletePrecondition(ws) ? 'enabled' : 'disabled';
135-
},
136-
callback: (scope) => {
137-
const ws = scope.block?.workspace;
138-
if (!ws) return;
139-
140-
// Delete the block(s), and put the cursor back in a sane location.
141-
return this.deleteCallback(ws, null);
142-
},
143-
scopeType: ContextMenuRegistry.ScopeType.BLOCK,
144-
id: 'blockDeleteFromContextMenu',
145-
weight: 11,
71+
return Msg['DELETE_BLOCK'].replace('%1', shortcut);
14672
};
14773

148-
ContextMenuRegistry.registry.register(deleteItem);
149-
}
150-
151-
/**
152-
* Precondition function for deleting a block from keyboard
153-
* navigation. This precondition is shared between keyboard shortcuts
154-
* and context menu items.
155-
*
156-
* @param workspace The `WorkspaceSvg` where the shortcut was
157-
* invoked.
158-
* @returns True iff `deleteCallback` function should be called.
159-
*/
160-
private deletePrecondition(workspace: WorkspaceSvg) {
161-
const sourceBlock = workspace.getCursor()?.getSourceBlock();
162-
return (
163-
!workspace.isDragging() &&
164-
this.navigation.canCurrentlyEdit(workspace) &&
165-
!!sourceBlock?.isDeletable()
166-
);
167-
}
168-
169-
/**
170-
* Callback function for deleting a block from keyboard
171-
* navigation. This callback is shared between keyboard shortcuts
172-
* and context menu items.
173-
*
174-
* @param workspace The `WorkspaceSvg` where the shortcut was
175-
* invoked.
176-
* @param e The originating event for a keyboard shortcut, or null
177-
* if called from a context menu.
178-
* @returns True if this function successfully handled deletion.
179-
*/
180-
private deleteCallback(workspace: WorkspaceSvg, e: Event | null) {
181-
const cursor = workspace.getCursor();
182-
if (!cursor) return false;
183-
184-
const sourceBlock = cursor.getSourceBlock();
185-
if (!sourceBlock) return false;
186-
// Delete or backspace.
187-
// There is an event if this is triggered from a keyboard shortcut,
188-
// but not if it's triggered from a context menu.
189-
if (e) {
190-
// Stop the browser from going back to the previous page.
191-
// Do this first to prevent an error in the delete code from resulting
192-
// in data loss.
193-
e.preventDefault();
194-
}
195-
// Don't delete while dragging. Jeez.
196-
if (Gesture.inProgress()) false;
197-
198-
if (cursor instanceof LineCursor) cursor.preDelete(sourceBlock);
199-
sourceBlock.checkAndDelete();
200-
if (cursor instanceof LineCursor) cursor.postDelete();
201-
return true;
74+
this.oldContextMenuItem.displayText = displayText;
20275
}
20376
}

src/constants.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ export enum SHORTCUT_NAMES {
4242
COPY = 'keyboard_nav_copy',
4343
CUT = 'keyboard_nav_cut',
4444
PASTE = 'keyboard_nav_paste',
45-
DELETE = 'keyboard_nav_delete',
4645
MOVE_WS_CURSOR_UP = 'workspace_up',
4746
MOVE_WS_CURSOR_DOWN = 'workspace_down',
4847
MOVE_WS_CURSOR_LEFT = 'workspace_left',

src/navigation.ts

Lines changed: 4 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,6 @@ import {
1717
registrationType as cursorRegistrationType,
1818
} from './flyout_cursor';
1919

20-
/**
21-
* The default coordinate to use when focusing on the workspace and no
22-
* blocks are present. In pixel coordinates, but will be converted to
23-
* workspace coordinates when used to position the cursor.
24-
*/
25-
const DEFAULT_WS_COORDINATE: Blockly.utils.Coordinate =
26-
new Blockly.utils.Coordinate(100, 100);
27-
28-
/**
29-
* The default coordinate to use when moving the cursor to the workspace
30-
* after a block has been deleted. In pixel coordinates, but will be
31-
* converted to workspace coordinates when used to position the cursor.
32-
*/
33-
const WS_COORDINATE_ON_DELETE: Blockly.utils.Coordinate =
34-
new Blockly.utils.Coordinate(100, 100);
35-
3620
/**
3721
* Class that holds all methods necessary for keyboard navigation to work.
3822
*/
@@ -181,18 +165,10 @@ export class Navigation {
181165
if (!workspace || !workspace.keyboardAccessibilityMode) {
182166
return;
183167
}
184-
switch (e.type) {
185-
case Blockly.Events.DELETE:
186-
this.handleBlockDeleteByDrag(
187-
workspace,
188-
e as Blockly.Events.BlockDelete,
189-
);
190-
break;
191-
case Blockly.Events.BLOCK_CHANGE:
192-
if ((e as Blockly.Events.BlockChange).element === 'mutation') {
193-
this.handleBlockMutation(workspace, e as Blockly.Events.BlockChange);
194-
}
195-
break;
168+
if (e.type === Blockly.Events.BLOCK_CHANGE) {
169+
if ((e as Blockly.Events.BlockChange).element === 'mutation') {
170+
this.handleBlockMutation(workspace, e as Blockly.Events.BlockChange);
171+
}
196172
}
197173
}
198174

@@ -289,31 +265,6 @@ export class Navigation {
289265
}
290266
}
291267

292-
/**
293-
* Moves the cursor to the workspace when its parent block is deleted by
294-
* being dragged to the flyout or to the trashcan.
295-
*
296-
* @param workspace The workspace the block was on.
297-
* @param e The event emitted when a block is deleted.
298-
*/
299-
handleBlockDeleteByDrag(
300-
workspace: Blockly.WorkspaceSvg,
301-
e: Blockly.Events.BlockDelete,
302-
) {
303-
const deletedBlockId = e.blockId;
304-
const ids = e.ids ?? [];
305-
const cursor = workspace.getCursor();
306-
if (!cursor) return;
307-
308-
// Make sure the cursor is on a block.
309-
const sourceBlock = cursor.getSourceBlock();
310-
if (!sourceBlock) return;
311-
312-
if (sourceBlock.id === deletedBlockId || ids.includes(sourceBlock.id)) {
313-
cursor.setCurNode(workspace);
314-
}
315-
}
316-
317268
/**
318269
* Handles when a user clicks on a block in the flyout by moving the cursor
319270
* to that stack of blocks and setting the state of navigation to the flyout.
@@ -397,10 +348,6 @@ export class Navigation {
397348
// disposed (which can happen when blocks are reloaded).
398349
return false;
399350
}
400-
const wsCoordinates = new Blockly.utils.Coordinate(
401-
DEFAULT_WS_COORDINATE.x / workspace.scale,
402-
DEFAULT_WS_COORDINATE.y / workspace.scale,
403-
);
404351
if (topBlocks.length > 0) {
405352
cursor.setCurNode(
406353
topBlocks[prefer === 'first' ? 0 : topBlocks.length - 1],

src/navigation_controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export class NavigationController {
5050
shortcutDialog: ShortcutDialog = new ShortcutDialog();
5151

5252
/** Context menu and keyboard action for deletion. */
53-
deleteAction: DeleteAction = new DeleteAction(this.navigation);
53+
deleteAction: DeleteAction = new DeleteAction();
5454

5555
/** Context menu and keyboard action for deletion. */
5656
editAction: EditAction = new EditAction(this.navigation);

0 commit comments

Comments
 (0)