Skip to content

Commit a250e90

Browse files
feat: keyboard navigation delete option in context menu (#159)
* feat: add keyboard nav delete option to block context menu * chore: add FIXME comments * chore: lint * chore: format navigation_controller * chore: code cleanup as suggested by reviewer Co-authored-by: Christopher Allen <[email protected]> * chore: code cleanup as suggested by reviewer 2 Co-authored-by: Christopher Allen <[email protected]> * chore: code cleanup as suggested by reviewer 3 Co-authored-by: Christopher Allen <[email protected]> --------- Co-authored-by: Christopher Allen <[email protected]>
1 parent 52ae76b commit a250e90

File tree

1 file changed

+111
-35
lines changed

1 file changed

+111
-35
lines changed

src/navigation_controller.ts

Lines changed: 111 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import * as Blockly from 'blockly/core';
1616
import {
1717
ASTNode,
1818
BlockSvg,
19+
ContextMenuRegistry,
1920
ICopyData,
2021
ShortcutRegistry,
2122
Toolbox,
@@ -224,6 +225,56 @@ export class NavigationController {
224225
return false;
225226
}
226227

228+
/**
229+
* Precondition function for deleting a block from keyboard
230+
* navigation. This precondition is shared between keyboard shortcuts
231+
* and context menu items.
232+
*
233+
* FIXME: This should be better encapsulated.
234+
*
235+
* @param workspace The `WorkspaceSvg` where the shortcut was
236+
* invoked.
237+
* @returns True iff `deleteCallbackFn` function should be called.
238+
*/
239+
protected deletePreconditionFn(workspace: WorkspaceSvg) {
240+
if (!this.canCurrentlyEdit(workspace)) return false;
241+
const sourceBlock = workspace.getCursor()?.getCurNode().getSourceBlock();
242+
return !!(sourceBlock?.isDeletable());
243+
}
244+
245+
/**
246+
* Callback function for deleting a block from keyboard
247+
* navigation. This callback is shared between keyboard shortcuts
248+
* and context menu items.
249+
*
250+
* FIXME: This should be better encapsulated.
251+
*
252+
* @param workspace The `WorkspaceSvg` where the shortcut was
253+
* invoked.
254+
* @param e The originating event for a keyboard shortcut, or null
255+
* if called from a context menu.
256+
* @returns True if this function successfully handled deletion.
257+
*/
258+
protected deleteCallbackFn(workspace: WorkspaceSvg, e: Event | null) {
259+
const cursor = workspace.getCursor();
260+
if (!cursor) return false;
261+
const sourceBlock = cursor.getCurNode().getSourceBlock() as BlockSvg;
262+
// Delete or backspace.
263+
// There is an event if this is triggered from a keyboard shortcut,
264+
// but not if it's triggered from a context menu.
265+
if (e) {
266+
// Stop the browser from going back to the previous page.
267+
// Do this first to prevent an error in the delete code from resulting
268+
// in data loss.
269+
e.preventDefault();
270+
}
271+
// Don't delete while dragging. Jeez.
272+
if (Blockly.Gesture.inProgress()) false;
273+
this.navigation.moveCursorOnBlockDelete(workspace, sourceBlock);
274+
sourceBlock.checkAndDelete();
275+
return true;
276+
}
277+
227278
/**
228279
* List all the currently registered shortcuts.
229280
*/
@@ -404,7 +455,7 @@ export class NavigationController {
404455
* - On the workspace: open the context menu.
405456
*/
406457
enter: {
407-
name: Constants.SHORTCUT_NAMES.MARK, // FIXME
458+
name: Constants.SHORTCUT_NAMES.MARK, // FIXME
408459
preconditionFn: (workspace) => this.canCurrentlyEdit(workspace),
409460
callback: (workspace) => {
410461
let flyoutCursor;
@@ -545,11 +596,11 @@ export class NavigationController {
545596
case Constants.STATE.WORKSPACE:
546597
const curNode = workspace?.getCursor()?.getCurNode();
547598
const source = curNode?.getSourceBlock();
548-
return !!(
549-
source?.isDeletable() &&
550-
source?.isMovable() &&
551-
!Blockly.Gesture.inProgress()
552-
);
599+
return !!(
600+
source?.isDeletable() &&
601+
source?.isMovable() &&
602+
!Blockly.Gesture.inProgress()
603+
);
553604
case Constants.STATE.FLYOUT:
554605
const flyoutWorkspace = workspace.getFlyout()?.getWorkspace();
555606
const sourceBlock = flyoutWorkspace
@@ -650,35 +701,8 @@ export class NavigationController {
650701
/** Keyboard shortcut to delete the block the cursor is currently on. */
651702
delete: {
652703
name: Constants.SHORTCUT_NAMES.DELETE,
653-
preconditionFn: (workspace) => {
654-
if (this.canCurrentlyEdit(workspace)) {
655-
const curNode = workspace.getCursor()?.getCurNode();
656-
if (curNode && curNode.getSourceBlock()) {
657-
const sourceBlock = curNode.getSourceBlock();
658-
return !!(sourceBlock && sourceBlock.isDeletable());
659-
}
660-
}
661-
return false;
662-
},
663-
callback: (workspace, e) => {
664-
const cursor = workspace.getCursor();
665-
if (!cursor) {
666-
return false;
667-
}
668-
const sourceBlock = cursor.getCurNode().getSourceBlock() as BlockSvg;
669-
// Delete or backspace.
670-
// Stop the browser from going back to the previous page.
671-
// Do this first to prevent an error in the delete code from resulting
672-
// in data loss.
673-
e.preventDefault();
674-
// Don't delete while dragging. Jeez.
675-
if (Blockly.Gesture.inProgress()) {
676-
return false;
677-
}
678-
this.navigation.moveCursorOnBlockDelete(workspace, sourceBlock);
679-
sourceBlock.checkAndDelete();
680-
return true;
681-
},
704+
preconditionFn: this.deletePreconditionFn,
705+
callback: this.deleteCallbackFn,
682706
keyCodes: [KeyCodes.DELETE, KeyCodes.BACKSPACE],
683707
allowCollision: true,
684708
},
@@ -827,6 +851,56 @@ export class NavigationController {
827851
},
828852
};
829853

854+
/**
855+
* Register the delete block action as a context menu item on blocks.
856+
* This function mixes together the keyboard and context menu preconditions
857+
* but only calls the keyboard callback.
858+
*/
859+
protected registerDeleteAction() {
860+
const originalDeleteItem =
861+
ContextMenuRegistry.registry.getItem('blockDelete');
862+
if (!originalDeleteItem) return;
863+
864+
const deleteItem: ContextMenuRegistry.RegistryItem = {
865+
displayText: (scope) => {
866+
// FIXME: Consider using the original delete item's display text,
867+
// which is dynamic based on the nubmer of blocks to delete.
868+
return 'Keyboard Navigation: delete';
869+
},
870+
preconditionFn: (scope) => {
871+
// FIXME: Find a better way to get the workspace, or use `as WorkspaceSvg`.
872+
const ws = scope.block?.workspace;
873+
874+
// Run the original precondition code, from the context menu option.
875+
// If the item would be hidden or disabled, respect it.
876+
const originalPreconditionResult =
877+
originalDeleteItem.preconditionFn(scope);
878+
if (!ws || originalPreconditionResult != 'enabled') {
879+
return originalPreconditionResult;
880+
}
881+
882+
// Return enabled if the keyboard shortcut precondition is allowed,
883+
// and disabled if the context menu precondition is met but the keyboard
884+
// shortcut precondition is not met.
885+
return this.deletePreconditionFn(ws) ? 'enabled' : 'disabled';
886+
},
887+
callback: (scope) => {
888+
// FIXME: Find a better way to get the workspace, or use `as WorkspaceSvg`.
889+
const ws = scope.block?.workspace;
890+
if (!ws) return;
891+
892+
// Delete the block(s), and put the cursor back in a sane location.
893+
return this.deleteCallbackFn(ws, null);
894+
},
895+
scopeType: ContextMenuRegistry.ScopeType.BLOCK,
896+
id: 'blockDeleteFromContextMenu',
897+
weight: 10,
898+
};
899+
900+
// FIXME: Decide whether to unregister the original item.
901+
ContextMenuRegistry.registry.register(deleteItem);
902+
}
903+
830904
/**
831905
* Registers all default keyboard shortcut items for keyboard
832906
* navigation. This should be called once per instance of
@@ -837,6 +911,8 @@ export class NavigationController {
837911
ShortcutRegistry.registry.register(shortcut);
838912
}
839913

914+
this.registerDeleteAction();
915+
840916
// Initalise the shortcut modal with available shortcuts. Needs
841917
// to be done separately rather at construction, as many shortcuts
842918
// are not registered at that point.

0 commit comments

Comments
 (0)