Skip to content

Commit cec4964

Browse files
Merge branch 'main' into toast
2 parents 973b6d3 + 4ff2247 commit cec4964

19 files changed

+916
-568
lines changed

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@blockly/keyboard-experiment",
3-
"version": "0.0.4",
3+
"version": "0.0.5",
44
"description": "A plugin for keyboard navigation.",
55
"scripts": {
66
"audit:fix": "blockly-scripts auditFix",
@@ -48,7 +48,7 @@
4848
"@types/p5": "^1.7.6",
4949
"@typescript-eslint/eslint-plugin": "^6.7.2",
5050
"@typescript-eslint/parser": "^6.7.2",
51-
"blockly": "^11.2.1",
51+
"blockly": "12.0.0-beta.2",
5252
"eslint": "^8.49.0",
5353
"eslint-config-google": "^0.14.0",
5454
"eslint-config-prettier": "^9.0.0",
@@ -61,7 +61,12 @@
6161
"typescript": "^5.4.5"
6262
},
6363
"peerDependencies": {
64-
"blockly": "^11.1.0"
64+
"blockly": "^12.0.0-beta.2"
65+
},
66+
"overrides": {
67+
"@blockly/field-colour": {
68+
"blockly": "^12.0.0-beta.2"
69+
}
6570
},
6671
"publishConfig": {
6772
"access": "public",

src/actions/action_menu.ts

Lines changed: 62 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
WidgetDiv,
1616
} from 'blockly';
1717
import * as Constants from '../constants';
18-
import type {BlockSvg, WorkspaceSvg} from 'blockly';
18+
import type {BlockSvg, RenderedConnection, WorkspaceSvg} from 'blockly';
1919
import {Navigation} from '../navigation';
2020

2121
const KeyCodes = BlocklyUtils.KeyCodes;
@@ -111,6 +111,7 @@ export class ActionMenu {
111111
const cursor = workspace.getCursor();
112112
if (!cursor) throw new Error('workspace has no cursor');
113113
const node = cursor.getCurNode();
114+
if (!node) throw new Error('No node is currently selected');
114115
const nodeType = node.getType();
115116
switch (nodeType) {
116117
case ASTNode.types.BLOCK:
@@ -140,34 +141,7 @@ export class ActionMenu {
140141
// Slightly hacky: get insert action from registry. Hacky
141142
// because registry typings don't include {connection: ...} as
142143
// a possible kind of scope.
143-
const insertAction = ContextMenuRegistry.registry.getItem('insert');
144-
if (!insertAction) throw new Error("can't find insert action");
145-
146-
const pasteAction = ContextMenuRegistry.registry.getItem(
147-
'blockPasteFromContextMenu',
148-
);
149-
if (!pasteAction) throw new Error("can't find paste action");
150-
const possibleOptions = [insertAction, pasteAction /* etc.*/];
151-
152-
// Check preconditions and get menu texts.
153-
const scope = {
154-
connection,
155-
} as unknown as ContextMenuRegistry.Scope;
156-
for (const option of possibleOptions) {
157-
const precondition = option.preconditionFn(scope);
158-
if (precondition === 'hidden') continue;
159-
const displayText =
160-
typeof option.displayText === 'function'
161-
? option.displayText(scope)
162-
: option.displayText;
163-
menuOptions.push({
164-
text: displayText,
165-
enabled: precondition === 'enabled',
166-
callback: option.callback,
167-
scope,
168-
weight: option.weight,
169-
});
170-
}
144+
this.addConnectionItems(connection, menuOptions);
171145
break;
172146

173147
default:
@@ -195,6 +169,50 @@ export class ActionMenu {
195169
return true;
196170
}
197171

172+
/**
173+
* Add menu items for a context menu on a connection scope.
174+
*
175+
* @param connection The connection on which the menu is shown.
176+
* @param menuOptions The list of options, which may be modified by this method.
177+
*/
178+
private addConnectionItems(
179+
connection: Connection,
180+
menuOptions: (
181+
| ContextMenuRegistry.ContextMenuOption
182+
| ContextMenuRegistry.LegacyContextMenuOption
183+
)[],
184+
) {
185+
const insertAction = ContextMenuRegistry.registry.getItem('insert');
186+
if (!insertAction) throw new Error("can't find insert action");
187+
188+
const pasteAction = ContextMenuRegistry.registry.getItem(
189+
'blockPasteFromContextMenu',
190+
);
191+
if (!pasteAction) throw new Error("can't find paste action");
192+
const possibleOptions = [insertAction, pasteAction /* etc.*/];
193+
194+
// Check preconditions and get menu texts.
195+
const scope = {
196+
connection,
197+
} as unknown as ContextMenuRegistry.Scope;
198+
for (const option of possibleOptions) {
199+
const precondition = option.preconditionFn?.(scope);
200+
if (precondition === 'hidden') continue;
201+
const displayText =
202+
(typeof option.displayText === 'function'
203+
? option.displayText(scope)
204+
: option.displayText) ?? '';
205+
menuOptions.push({
206+
text: displayText,
207+
enabled: precondition === 'enabled',
208+
callback: option.callback!,
209+
scope,
210+
weight: option.weight,
211+
});
212+
}
213+
return menuOptions;
214+
}
215+
198216
/**
199217
* Create a fake PointerEvent for opening the action menu for the
200218
* given ASTNode.
@@ -205,31 +223,28 @@ export class ActionMenu {
205223
private fakeEventForNode(node: ASTNode): PointerEvent {
206224
switch (node.getType()) {
207225
case ASTNode.types.BLOCK:
208-
return this.fakeEventForBlockNode(node);
226+
return this.fakeEventForBlock(node.getLocation() as BlockSvg);
209227
case ASTNode.types.NEXT:
210228
case ASTNode.types.PREVIOUS:
211229
case ASTNode.types.INPUT:
212-
return this.fakeEventForConnectionNode(node);
230+
return this.fakeEventForConnectionNode(
231+
node.getLocation() as RenderedConnection,
232+
);
213233
default:
214234
throw new TypeError('unhandled node type');
215235
}
216236
}
217237

218238
/**
219-
* Create a fake PointerEvent for opening the action menu for the
220-
* given ASTNode of type BLOCK.
239+
* Create a fake PointerEvent for opening the action menu on the specified
240+
* block.
221241
*
222-
* @param node The node to open the action menu for.
242+
* @param block The block to open the action menu for.
223243
* @returns A synthetic pointerdown PointerEvent.
224244
*/
225-
private fakeEventForBlockNode(node: ASTNode): PointerEvent {
226-
if (node.getType() !== ASTNode.types.BLOCK) {
227-
throw new TypeError('can only create PointerEvents for BLOCK nodes');
228-
}
229-
245+
private fakeEventForBlock(block: BlockSvg) {
230246
// Get the location of the top-left corner of the block in
231247
// screen coordinates.
232-
const block = node.getLocation() as BlockSvg;
233248
const blockCoords = BlocklyUtils.svgMath.wsToScreenCoordinates(
234249
block.workspace,
235250
block.getRelativeToSurfaceXY(),
@@ -257,31 +272,23 @@ export class ActionMenu {
257272

258273
/**
259274
* Create a fake PointerEvent for opening the action menu for the
260-
* given ASTNode of type NEXT, PREVIOUS or INPUT.
275+
* given connection.
261276
*
262277
* For now this just puts the action menu in the same place as the
263278
* context menu for the source block.
264279
*
265-
* @param node The node to open the action menu for.
280+
* @param connection The node to open the action menu for.
266281
* @returns A synthetic pointerdown PointerEvent.
267282
*/
268-
private fakeEventForConnectionNode(node: ASTNode): PointerEvent {
269-
if (
270-
node.getType() !== ASTNode.types.NEXT &&
271-
node.getType() !== ASTNode.types.PREVIOUS &&
272-
node.getType() !== ASTNode.types.INPUT
273-
) {
274-
throw new TypeError('can only create PointerEvents for connection nodes');
275-
}
276-
277-
const connection = node.getLocation() as Connection;
278-
const block = connection.getSourceBlock();
283+
private fakeEventForConnectionNode(
284+
connection: RenderedConnection,
285+
): PointerEvent {
286+
const block = connection.getSourceBlock() as BlockSvg;
279287
const workspace = block.workspace as WorkspaceSvg;
280288

281289
if (typeof connection.x !== 'number') {
282290
// No coordinates for connection? Fall back to the parent block.
283-
const blockNode = new ASTNode(ASTNode.types.BLOCK, block);
284-
return this.fakeEventForBlockNode(blockNode);
291+
return this.fakeEventForBlock(block);
285292
}
286293
const connectionWSCoords = new BlocklyUtils.Coordinate(
287294
connection.x,

src/actions/arrow_navigation.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class ArrowNavigation {
4040
return false;
4141
}
4242
const curNode = cursor.getCurNode();
43-
if (curNode.getType() === ASTNode.types.FIELD) {
43+
if (curNode?.getType() === ASTNode.types.FIELD) {
4444
return (curNode.getLocation() as Field).onShortcut(shortcut);
4545
}
4646
return false;
@@ -64,7 +64,9 @@ export class ArrowNavigation {
6464
case Constants.STATE.WORKSPACE:
6565
isHandled = this.fieldShortcutHandler(workspace, shortcut);
6666
if (!isHandled && workspace) {
67-
workspace.getCursor()?.in();
67+
if (!this.navigation.defaultCursorPositionIfNeeded(workspace)) {
68+
workspace.getCursor()?.in();
69+
}
6870
isHandled = true;
6971
}
7072
return isHandled;
@@ -95,7 +97,9 @@ export class ArrowNavigation {
9597
case Constants.STATE.WORKSPACE:
9698
isHandled = this.fieldShortcutHandler(workspace, shortcut);
9799
if (!isHandled && workspace) {
98-
workspace.getCursor()?.out();
100+
if (!this.navigation.defaultCursorPositionIfNeeded(workspace)) {
101+
workspace.getCursor()?.out();
102+
}
99103
isHandled = true;
100104
}
101105
return isHandled;
@@ -125,7 +129,9 @@ export class ArrowNavigation {
125129
case Constants.STATE.WORKSPACE:
126130
isHandled = this.fieldShortcutHandler(workspace, shortcut);
127131
if (!isHandled && workspace) {
128-
workspace.getCursor()?.next();
132+
if (!this.navigation.defaultCursorPositionIfNeeded(workspace)) {
133+
workspace.getCursor()?.next();
134+
}
129135
isHandled = true;
130136
}
131137
return isHandled;
@@ -158,7 +164,14 @@ export class ArrowNavigation {
158164
case Constants.STATE.WORKSPACE:
159165
isHandled = this.fieldShortcutHandler(workspace, shortcut);
160166
if (!isHandled) {
161-
workspace.getCursor()?.prev();
167+
if (
168+
!this.navigation.defaultCursorPositionIfNeeded(
169+
workspace,
170+
'last',
171+
)
172+
) {
173+
workspace.getCursor()?.prev();
174+
}
162175
isHandled = true;
163176
}
164177
return isHandled;

src/actions/clipboard.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const createSerializedKey = ShortcutRegistry.registry.createSerializedKey.bind(
3232
* menu; changing individual weights relative to base weight can change
3333
* the order within the clipboard group.
3434
*/
35-
const BASE_WEIGHT = 11;
35+
const BASE_WEIGHT = 12;
3636

3737
/**
3838
* Logic and state for cut/copy/paste actions as both keyboard shortcuts
@@ -171,7 +171,9 @@ export class Clipboard {
171171
private cutCallback(workspace: WorkspaceSvg) {
172172
const cursor = workspace.getCursor();
173173
if (!cursor) throw new TypeError('no cursor');
174-
const sourceBlock = cursor.getCurNode().getSourceBlock() as BlockSvg | null;
174+
const sourceBlock = cursor
175+
.getCurNode()
176+
?.getSourceBlock() as BlockSvg | null;
175177
if (!sourceBlock) throw new TypeError('no source block');
176178
this.copyData = sourceBlock.toCopyData();
177179
this.copyWorkspace = sourceBlock.workspace;
@@ -276,17 +278,22 @@ export class Clipboard {
276278
const sourceBlock = activeWorkspace
277279
?.getCursor()
278280
?.getCurNode()
279-
.getSourceBlock() as BlockSvg;
280-
workspace.hideChaff();
281+
?.getSourceBlock() as BlockSvg;
282+
if (!sourceBlock) return false;
283+
281284
this.copyData = sourceBlock.toCopyData();
282285
this.copyWorkspace = sourceBlock.workspace;
283-
if (this.copyData) {
286+
const copied = !!this.copyData;
287+
if (copied) {
288+
if (navigationState === Constants.STATE.FLYOUT) {
289+
this.navigation.focusWorkspace(workspace);
290+
}
284291
toast(workspace, {
285-
message: `Copied. Press ${formatMetaShortcut("V")} to paste.`,
292+
message: `Copied. Press ${formatMetaShortcut('V')} to paste.`,
286293
duration: 4500,
287294
});
288295
}
289-
return !!this.copyData;
296+
return copied;
290297
}
291298

292299
/**
@@ -368,8 +375,10 @@ export class Clipboard {
368375
? workspace
369376
: this.copyWorkspace;
370377

371-
// Do this before clipoard.paste due to cursor/focus workaround in getCurNode.
372-
const targetNode = pasteWorkspace.getCursor()?.getCurNode();
378+
const targetNode = this.navigation.getStationaryNode(pasteWorkspace);
379+
// If we're pasting in the flyout it still targets the workspace. Focus first
380+
// so ensure correct selection handling.
381+
this.navigation.focusWorkspace(workspace);
373382

374383
Events.setGroup(true);
375384
const block = clipboard.paste(this.copyData, pasteWorkspace) as BlockSvg;
@@ -381,7 +390,6 @@ export class Clipboard {
381390
ASTNode.createBlockNode(block)!,
382391
);
383392
}
384-
this.navigation.removeMark(pasteWorkspace);
385393
Events.setGroup(false);
386394
return true;
387395
}

0 commit comments

Comments
 (0)