Skip to content

Commit a3c9562

Browse files
Merge branch 'main' into field-edit-focus-style
2 parents c1234c9 + d5151f2 commit a3c9562

24 files changed

+939
-405
lines changed

.github/workflows/pages.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ jobs:
6464
uses: actions/upload-pages-artifact@v3
6565
with:
6666
# Upload build folder
67-
path: './blockly-keyboard-experimentation/dist'
67+
path: './blockly-keyboard-experimentation/build'
6868

6969
deploy:
7070
environment:

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
"scripts": {
66
"audit:fix": "blockly-scripts auditFix",
77
"build": "blockly-scripts build",
8-
"ghpages": "webpack",
98
"clean": "blockly-scripts clean",
109
"lint": "eslint .",
1110
"lint:fix": "eslint . --fix",
1211
"format": "prettier --write .",
1312
"format:check": "prettier --check .",
13+
"ghpages": "node scripts/deploy.js",
1414
"predeploy": "blockly-scripts predeploy",
1515
"prepublishOnly": "npm login --registry https://wombat-dressing-room.appspot.com",
1616
"start": "blockly-scripts start",

scripts/deploy.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
const execSync = require('child_process').execSync;
8+
const fs = require('fs');
9+
10+
console.log(`Preparing test page for gh-pages deployment.`);
11+
12+
execSync(`npm run build && npm run predeploy`, {stdio: 'pipe'});
13+
14+
// Copy test/index.html to build/ directory.
15+
// Update the path at which the test_bundle can be found.
16+
let testPage = fs.readFileSync('./test/index.html').toString();
17+
testPage = testPage.replace('../build/test_bundle.js', 'test_bundle.js');
18+
fs.writeFileSync('build/index.html', testPage, 'utf-8');
19+
console.log(
20+
`Open 'build/index.html' in a browser to see results, or upload the 'build' directory to ghpages.`,
21+
);

src/actions/duplicate.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {
8+
BlockSvg,
9+
clipboard,
10+
ContextMenuRegistry,
11+
ICopyable,
12+
ShortcutRegistry,
13+
utils,
14+
comments,
15+
ICopyData,
16+
} from 'blockly';
17+
import * as Constants from '../constants';
18+
import {getMenuItem} from '../shortcut_formatting';
19+
20+
/**
21+
* Duplicate action that adds a keyboard shortcut for duplicate and overrides
22+
* the context menu item to show it if the context menu item is registered.
23+
*/
24+
export class DuplicateAction {
25+
private duplicateShortcut: ShortcutRegistry.KeyboardShortcut | null = null;
26+
private uninstallHandlers: Array<() => void> = [];
27+
28+
/**
29+
* Install the shortcuts and override context menu entries.
30+
*
31+
* No change is made if there's already a 'duplicate' shortcut.
32+
*/
33+
install() {
34+
this.duplicateShortcut = this.registerDuplicateShortcut();
35+
if (this.duplicateShortcut) {
36+
this.uninstallHandlers.push(
37+
overrideContextMenuItemForShortcutText(
38+
'blockDuplicate',
39+
Constants.SHORTCUT_NAMES.DUPLICATE,
40+
),
41+
);
42+
this.uninstallHandlers.push(
43+
overrideContextMenuItemForShortcutText(
44+
'commentDuplicate',
45+
Constants.SHORTCUT_NAMES.DUPLICATE,
46+
),
47+
);
48+
}
49+
}
50+
51+
/**
52+
* Unregister the shortcut and reinstate the original context menu entries.
53+
*/
54+
uninstall() {
55+
this.uninstallHandlers.forEach((handler) => handler());
56+
this.uninstallHandlers.length = 0;
57+
if (this.duplicateShortcut) {
58+
ShortcutRegistry.registry.unregister(this.duplicateShortcut.name);
59+
}
60+
}
61+
62+
/**
63+
* Create and register the keyboard shortcut for the duplicate action.
64+
* Same behaviour as for the core context menu.
65+
* Skipped if there is a shortcut with a matching name already.
66+
*/
67+
private registerDuplicateShortcut(): ShortcutRegistry.KeyboardShortcut | null {
68+
if (
69+
ShortcutRegistry.registry.getRegistry()[
70+
Constants.SHORTCUT_NAMES.DUPLICATE
71+
]
72+
) {
73+
return null;
74+
}
75+
76+
const shortcut: ShortcutRegistry.KeyboardShortcut = {
77+
name: Constants.SHORTCUT_NAMES.DUPLICATE,
78+
// Equivalent to the core context menu entry.
79+
preconditionFn(workspace, scope) {
80+
const {focusedNode} = scope;
81+
if (focusedNode instanceof BlockSvg) {
82+
return (
83+
!focusedNode.isInFlyout &&
84+
focusedNode.isDeletable() &&
85+
focusedNode.isMovable() &&
86+
focusedNode.isDuplicatable()
87+
);
88+
} else if (focusedNode instanceof comments.RenderedWorkspaceComment) {
89+
return focusedNode.isMovable();
90+
}
91+
return false;
92+
},
93+
callback(workspace, e, shortcut, scope) {
94+
const copyable = scope.focusedNode as ICopyable<ICopyData>;
95+
const data = copyable.toCopyData();
96+
if (!data) return false;
97+
return !!clipboard.paste(data, workspace);
98+
},
99+
keyCodes: [utils.KeyCodes.D],
100+
};
101+
ShortcutRegistry.registry.register(shortcut);
102+
return shortcut;
103+
}
104+
}
105+
106+
/**
107+
* Replace a context menu item to add shortcut text to its displayText.
108+
*
109+
* Nothing happens if there is not a matching context menu item registered.
110+
*
111+
* @param registryId Context menu registry id to replace if present.
112+
* @param shortcutName The corresponding shortcut name.
113+
* @returns A function to reinstate the original context menu entry.
114+
*/
115+
function overrideContextMenuItemForShortcutText(
116+
registryId: string,
117+
shortcutName: string,
118+
): () => void {
119+
const original = ContextMenuRegistry.registry.getItem(registryId);
120+
if (!original || 'separator' in original) {
121+
return () => {};
122+
}
123+
124+
const override: ContextMenuRegistry.RegistryItem = {
125+
...original,
126+
displayText: (scope: ContextMenuRegistry.Scope) => {
127+
const displayText =
128+
typeof original.displayText === 'function'
129+
? original.displayText(scope)
130+
: original.displayText;
131+
if (displayText instanceof HTMLElement) {
132+
// We can't cope in this scenario.
133+
return displayText;
134+
}
135+
return getMenuItem(displayText, shortcutName);
136+
},
137+
};
138+
ContextMenuRegistry.registry.unregister(registryId);
139+
ContextMenuRegistry.registry.register(override);
140+
141+
return () => {
142+
ContextMenuRegistry.registry.unregister(registryId);
143+
ContextMenuRegistry.registry.register(original);
144+
};
145+
}

src/actions/enter.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ import {
1616
icons,
1717
FocusableTreeTraverser,
1818
renderManagement,
19+
comments,
20+
getFocusManager,
1921
} from 'blockly/core';
2022

2123
import type {Block} from 'blockly/core';
2224

2325
import * as Constants from '../constants';
2426
import type {Navigation} from '../navigation';
25-
import {Mover} from './mover';
27+
import {Mover, MoveType} from './mover';
2628
import {
2729
showConstrainedMovementHint,
2830
showHelpHint,
@@ -124,10 +126,13 @@ export class EnterAction {
124126
) {
125127
return !workspace.isReadOnly();
126128
}
127-
if (curNode instanceof BlockSvg) return true;
128129
// Returning true is sometimes incorrect for icons, but there's no API to check.
129-
if (curNode instanceof icons.Icon) return true;
130-
return false;
130+
return (
131+
curNode instanceof BlockSvg ||
132+
curNode instanceof icons.Icon ||
133+
curNode instanceof comments.CommentBarButton ||
134+
curNode instanceof comments.RenderedWorkspaceComment
135+
);
131136
}
132137

133138
/**
@@ -159,9 +164,20 @@ export class EnterAction {
159164
// opening a bubble of some sort. We then need to wait for the bubble to
160165
// appear before attempting to navigate into it.
161166
curNode.onClick();
162-
renderManagement.finishQueuedRenders().then(() => {
163-
cursor?.in();
164-
});
167+
// This currently only works for MutatorIcons.
168+
// See icon_navigation_policy.
169+
if (curNode instanceof icons.MutatorIcon) {
170+
renderManagement.finishQueuedRenders().then(() => {
171+
cursor?.in();
172+
});
173+
}
174+
return true;
175+
} else if (curNode instanceof comments.CommentBarButton) {
176+
curNode.performAction();
177+
return true;
178+
} else if (curNode instanceof comments.RenderedWorkspaceComment) {
179+
curNode.setCollapsed(false);
180+
getFocusManager().focusNode(curNode.getEditorFocusableNode());
165181
return true;
166182
}
167183
return false;
@@ -194,13 +210,19 @@ export class EnterAction {
194210
const insertStartPoint = stationaryNode
195211
? this.navigation.findInsertStartPoint(stationaryNode, newBlock)
196212
: null;
213+
197214
if (workspace.getTopBlocks().includes(newBlock)) {
198215
this.positionNewTopLevelBlock(workspace, newBlock);
199216
}
200217

201218
workspace.setResizesEnabled(true);
202219

203-
this.mover.startMove(workspace, newBlock, insertStartPoint);
220+
this.mover.startMove(
221+
workspace,
222+
newBlock,
223+
MoveType.Insert,
224+
insertStartPoint,
225+
);
204226

205227
const isStartBlock =
206228
!newBlock.outputConnection &&

0 commit comments

Comments
 (0)