mirror of
https://github.com/google/blockly.git
synced 2026-04-28 08:00:20 +02:00
Merge pull request #9606 from RaspberryPiFoundation/main
chore: Merge `main` into `v13`
This commit is contained in:
Generated
+1
-1
@@ -1306,7 +1306,7 @@
|
||||
}
|
||||
},
|
||||
"packages/blockly": {
|
||||
"version": "12.3.1",
|
||||
"version": "12.4.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -870,6 +870,32 @@ export class BlockSvg
|
||||
return this.svgGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the closest live block to this one, if any.
|
||||
*/
|
||||
private getNearestNeighbour() {
|
||||
if (!this.workspace.rendered) return null;
|
||||
|
||||
const blocks = this.workspace
|
||||
.getAllBlocks(false)
|
||||
.filter((block) => !block.isDeadOrDying());
|
||||
let nearestNeighbour = null;
|
||||
let closestDistance = Number.MAX_SAFE_INTEGER;
|
||||
const self = this.getRelativeToSurfaceXY();
|
||||
for (const block of blocks) {
|
||||
const other = block.getRelativeToSurfaceXY();
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(other.x - self.x, 2) + Math.pow(other.y - self.y, 2),
|
||||
);
|
||||
if (distance < closestDistance) {
|
||||
nearestNeighbour = block;
|
||||
closestDistance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
return nearestNeighbour;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of this block.
|
||||
*
|
||||
@@ -911,7 +937,15 @@ export class BlockSvg
|
||||
if (parent) {
|
||||
focusManager.focusNode(parent);
|
||||
} else {
|
||||
setTimeout(() => focusManager.focusTree(this.workspace), 0);
|
||||
const nearestNeighbour = this.getNearestNeighbour();
|
||||
if (nearestNeighbour) {
|
||||
focusManager.focusNode(nearestNeighbour);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
if (!this.workspace.rendered) return;
|
||||
focusManager.focusTree(this.workspace);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -238,6 +238,8 @@ function haltPropagation(e: Event) {
|
||||
export function hide() {
|
||||
WidgetDiv.hideIfOwner(dummyOwner);
|
||||
currentBlock = null;
|
||||
menu_?.dispose();
|
||||
menu_ = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -293,3 +295,10 @@ export function callbackFactory(
|
||||
return newBlock;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contextual menu if it is currently being shown.
|
||||
*/
|
||||
export function getMenu(): Menu | null {
|
||||
return menu_;
|
||||
}
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
import {BlockSvg} from './block_svg.js';
|
||||
import * as clipboard from './clipboard.js';
|
||||
import {RenderedWorkspaceComment} from './comments.js';
|
||||
import * as contextmenu from './contextmenu.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import {getFocusManager} from './focus_manager.js';
|
||||
import {hasContextMenu} from './interfaces/i_contextmenu.js';
|
||||
import {isCopyable as isICopyable} from './interfaces/i_copyable.js';
|
||||
import {isDeletable as isIDeletable} from './interfaces/i_deletable.js';
|
||||
import {isDraggable} from './interfaces/i_draggable.js';
|
||||
@@ -33,6 +35,7 @@ export enum names {
|
||||
PASTE = 'paste',
|
||||
UNDO = 'undo',
|
||||
REDO = 'redo',
|
||||
MENU = 'menu',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,10 +137,7 @@ function isCuttable(focused: IFocusableNode): boolean {
|
||||
*/
|
||||
export function registerCopy() {
|
||||
const ctrlC = ShortcutRegistry.registry.createSerializedKey(KeyCodes.C, [
|
||||
KeyCodes.CTRL,
|
||||
]);
|
||||
const metaC = ShortcutRegistry.registry.createSerializedKey(KeyCodes.C, [
|
||||
KeyCodes.META,
|
||||
KeyCodes.CTRL_CMD,
|
||||
]);
|
||||
|
||||
const copyShortcut: KeyboardShortcut = {
|
||||
@@ -179,7 +179,7 @@ export function registerCopy() {
|
||||
: undefined;
|
||||
return !!clipboard.copy(focused, copyCoords);
|
||||
},
|
||||
keyCodes: [ctrlC, metaC],
|
||||
keyCodes: [ctrlC],
|
||||
};
|
||||
ShortcutRegistry.registry.register(copyShortcut);
|
||||
}
|
||||
@@ -189,10 +189,7 @@ export function registerCopy() {
|
||||
*/
|
||||
export function registerCut() {
|
||||
const ctrlX = ShortcutRegistry.registry.createSerializedKey(KeyCodes.X, [
|
||||
KeyCodes.CTRL,
|
||||
]);
|
||||
const metaX = ShortcutRegistry.registry.createSerializedKey(KeyCodes.X, [
|
||||
KeyCodes.META,
|
||||
KeyCodes.CTRL_CMD,
|
||||
]);
|
||||
|
||||
const cutShortcut: KeyboardShortcut = {
|
||||
@@ -224,7 +221,7 @@ export function registerCut() {
|
||||
}
|
||||
return !!copyData;
|
||||
},
|
||||
keyCodes: [ctrlX, metaX],
|
||||
keyCodes: [ctrlX],
|
||||
};
|
||||
|
||||
ShortcutRegistry.registry.register(cutShortcut);
|
||||
@@ -235,10 +232,7 @@ export function registerCut() {
|
||||
*/
|
||||
export function registerPaste() {
|
||||
const ctrlV = ShortcutRegistry.registry.createSerializedKey(KeyCodes.V, [
|
||||
KeyCodes.CTRL,
|
||||
]);
|
||||
const metaV = ShortcutRegistry.registry.createSerializedKey(KeyCodes.V, [
|
||||
KeyCodes.META,
|
||||
KeyCodes.CTRL_CMD,
|
||||
]);
|
||||
|
||||
const pasteShortcut: KeyboardShortcut = {
|
||||
@@ -309,7 +303,7 @@ export function registerPaste() {
|
||||
const centerCoords = new Coordinate(left + width / 2, top + height / 2);
|
||||
return !!clipboard.paste(copyData, targetWorkspace, centerCoords);
|
||||
},
|
||||
keyCodes: [ctrlV, metaV],
|
||||
keyCodes: [ctrlV],
|
||||
};
|
||||
|
||||
ShortcutRegistry.registry.register(pasteShortcut);
|
||||
@@ -320,10 +314,7 @@ export function registerPaste() {
|
||||
*/
|
||||
export function registerUndo() {
|
||||
const ctrlZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [
|
||||
KeyCodes.CTRL,
|
||||
]);
|
||||
const metaZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [
|
||||
KeyCodes.META,
|
||||
KeyCodes.CTRL_CMD,
|
||||
]);
|
||||
|
||||
const undoShortcut: KeyboardShortcut = {
|
||||
@@ -342,7 +333,7 @@ export function registerUndo() {
|
||||
e.preventDefault();
|
||||
return true;
|
||||
},
|
||||
keyCodes: [ctrlZ, metaZ],
|
||||
keyCodes: [ctrlZ],
|
||||
};
|
||||
ShortcutRegistry.registry.register(undoShortcut);
|
||||
}
|
||||
@@ -353,13 +344,10 @@ export function registerUndo() {
|
||||
*/
|
||||
export function registerRedo() {
|
||||
const ctrlShiftZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [
|
||||
KeyCodes.CTRL,
|
||||
KeyCodes.SHIFT,
|
||||
]);
|
||||
const metaShiftZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [
|
||||
KeyCodes.META,
|
||||
KeyCodes.CTRL_CMD,
|
||||
KeyCodes.SHIFT,
|
||||
]);
|
||||
|
||||
// Ctrl-y is redo in Windows. Command-y is never valid on Macs.
|
||||
const ctrlY = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Y, [
|
||||
KeyCodes.CTRL,
|
||||
@@ -381,11 +369,40 @@ export function registerRedo() {
|
||||
e.preventDefault();
|
||||
return true;
|
||||
},
|
||||
keyCodes: [ctrlShiftZ, metaShiftZ, ctrlY],
|
||||
keyCodes: [ctrlShiftZ, ctrlY],
|
||||
};
|
||||
ShortcutRegistry.registry.register(redoShortcut);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyboard shortcut to show the context menu on ctrl/cmd+Enter.
|
||||
*/
|
||||
export function registerShowContextMenu() {
|
||||
const ctrlEnter = ShortcutRegistry.registry.createSerializedKey(
|
||||
KeyCodes.ENTER,
|
||||
[KeyCodes.CTRL_CMD],
|
||||
);
|
||||
|
||||
const contextMenuShortcut: KeyboardShortcut = {
|
||||
name: names.MENU,
|
||||
preconditionFn: (workspace) => {
|
||||
return !workspace.isDragging();
|
||||
},
|
||||
callback: (workspace, e) => {
|
||||
const target = getFocusManager().getFocusedNode();
|
||||
if (hasContextMenu(target)) {
|
||||
target.showContextMenu(e);
|
||||
contextmenu.getMenu()?.highlightNext();
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
keyCodes: [ctrlEnter],
|
||||
};
|
||||
ShortcutRegistry.registry.register(contextMenuShortcut);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers all default keyboard shortcut item. This should be called once per
|
||||
* instance of KeyboardShortcutRegistry.
|
||||
@@ -400,6 +417,7 @@ export function registerDefaultShortcuts() {
|
||||
registerPaste();
|
||||
registerUndo();
|
||||
registerRedo();
|
||||
registerShowContextMenu();
|
||||
}
|
||||
|
||||
registerDefaultShortcuts();
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as userAgent from '../utils/useragent.js';
|
||||
|
||||
// Former goog.module ID: Blockly.utils.KeyCodes
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-duplicate-enum-values */
|
||||
@@ -151,4 +153,10 @@ export enum KeyCodes {
|
||||
// indicates a hardware/bios problem.
|
||||
// http://en.community.dell.com/support-forums/laptop/f/3518/p/19285957/19523128.aspx
|
||||
PHANTOM = 255,
|
||||
|
||||
// The primary modifier key on the current platform, i.e. Command on Apple
|
||||
// platforms and Control elsewhere.
|
||||
CTRL_CMD = userAgent.MAC || userAgent.IPHONE || userAgent.IPAD
|
||||
? MAC_WK_CMD_LEFT
|
||||
: CTRL,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "blockly",
|
||||
"version": "12.3.1",
|
||||
"version": "12.4.1",
|
||||
"description": "Blockly is a library for building visual programming editors.",
|
||||
"keywords": [
|
||||
"blockly"
|
||||
|
||||
@@ -2946,4 +2946,119 @@ suite('Blocks', function () {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Disposal focus management', function () {
|
||||
setup(function () {
|
||||
this.workspace = Blockly.inject('blocklyDiv');
|
||||
const firstBlock = this.workspace.newBlock('stack_block');
|
||||
firstBlock.moveBy(-500, -500);
|
||||
});
|
||||
|
||||
test('Deleting the sole block on the workspace focuses the workspace', function () {
|
||||
const block = this.workspace.getTopBlocks(false)[0];
|
||||
Blockly.getFocusManager().focusNode(block);
|
||||
block.dispose();
|
||||
this.clock.runAll();
|
||||
|
||||
assert.strictEqual(
|
||||
Blockly.getFocusManager().getFocusedNode(),
|
||||
this.workspace,
|
||||
'Focus should move to the workspace when the focused block is deleted',
|
||||
);
|
||||
});
|
||||
|
||||
test('Deleting a block with several adjacent blocks focuses the closest one', function () {
|
||||
this.workspace.newBlock('stack_block');
|
||||
const blockMiddle = this.workspace.newBlock('stack_block');
|
||||
const blockRight = this.workspace.newBlock('stack_block');
|
||||
blockMiddle.moveBy(60, 0);
|
||||
blockRight.moveBy(100, 0);
|
||||
|
||||
Blockly.getFocusManager().focusNode(blockMiddle);
|
||||
blockMiddle.dispose();
|
||||
this.clock.runAll();
|
||||
|
||||
const focused = Blockly.getFocusManager().getFocusedNode();
|
||||
assert.strictEqual(
|
||||
focused,
|
||||
blockRight,
|
||||
'Focus should move to the closest remaining block (blockRight at (100, 0))',
|
||||
);
|
||||
});
|
||||
|
||||
test('Bulk deleting blocks does not focus another dying block', function () {
|
||||
const blocks = this.workspace.getTopBlocks(false);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
blocks.push(this.workspace.newBlock('stack_block'));
|
||||
}
|
||||
|
||||
// Focus the last block we added; clearing the workspace proceeds in block
|
||||
// creation order, so if we focused an earlier block, it would (correctly)
|
||||
// assign focus to a later-added block which is not yet dying, on down the
|
||||
// chain. If we focus the last block, by the time deletion gets to it, all
|
||||
// the other blocks will have already been marked as disposing, and should
|
||||
// thus be ineligible to be focused.
|
||||
Blockly.getFocusManager().focusNode(
|
||||
this.workspace.getTopBlocks(false)[5],
|
||||
);
|
||||
|
||||
const spy = sinon.spy(Blockly.getFocusManager(), 'focusNode');
|
||||
|
||||
this.workspace.clear();
|
||||
this.clock.runAll();
|
||||
|
||||
for (const block of blocks) {
|
||||
assert.isFalse(spy.calledWith(block));
|
||||
}
|
||||
assert.strictEqual(
|
||||
Blockly.getFocusManager().getFocusedNode(),
|
||||
this.workspace,
|
||||
'Focus should move to the workspace, not a dying peer block',
|
||||
);
|
||||
|
||||
spy.restore();
|
||||
});
|
||||
|
||||
test('Deleting a block focuses its parent block', function () {
|
||||
const parent = this.workspace.newBlock('stack_block');
|
||||
const child = this.workspace.newBlock('stack_block');
|
||||
parent.nextConnection.connect(child.previousConnection);
|
||||
|
||||
Blockly.getFocusManager().focusNode(child);
|
||||
child.dispose();
|
||||
this.clock.runAll();
|
||||
|
||||
assert.strictEqual(
|
||||
Blockly.getFocusManager().getFocusedNode(),
|
||||
parent,
|
||||
'Focus should move to the parent block when a connected child is deleted',
|
||||
);
|
||||
});
|
||||
|
||||
test('Deleting an unfocused block does not change focus', function () {
|
||||
const a = this.workspace.getTopBlocks(false)[0];
|
||||
const b = this.workspace.newBlock('stack_block');
|
||||
this.workspace.newBlock('stack_block');
|
||||
|
||||
Blockly.getFocusManager().focusNode(a);
|
||||
b.dispose();
|
||||
this.clock.runAll();
|
||||
|
||||
assert.strictEqual(
|
||||
Blockly.getFocusManager().getFocusedNode(),
|
||||
a,
|
||||
'Focus should not change when an unfocused block is deleted',
|
||||
);
|
||||
});
|
||||
|
||||
test('Disposing a workspace with a focused block succeeds', function () {
|
||||
Blockly.getFocusManager().focusNode(
|
||||
this.workspace.getTopBlocks(false)[0],
|
||||
);
|
||||
this.workspace.dispose();
|
||||
this.clock.runAll();
|
||||
|
||||
// No assert, this just shouldn't throw.
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -70,4 +70,25 @@ suite('Context Menu', function () {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('getMenu', function () {
|
||||
test('returns null when context menu is not shown', function () {
|
||||
assert.isNull(Blockly.ContextMenu.getMenu());
|
||||
});
|
||||
|
||||
test('returns Menu instance when context menu is shown', function () {
|
||||
const e = new PointerEvent('pointerdown', {clientX: 10, clientY: 10});
|
||||
const menuOptions = [
|
||||
{text: 'Test option', enabled: true, callback: function () {}},
|
||||
];
|
||||
Blockly.ContextMenu.show(e, menuOptions, false, this.workspace);
|
||||
|
||||
const menu = Blockly.ContextMenu.getMenu();
|
||||
assert.instanceOf(menu, Blockly.Menu, 'getMenu() should return a Menu');
|
||||
assert.include(menu.getElement().innerText, 'Test option');
|
||||
|
||||
Blockly.ContextMenu.hide();
|
||||
assert.isNull(Blockly.ContextMenu.getMenu());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,8 @@ suite('Keyboard Shortcut Items', function () {
|
||||
sharedTestSetup.call(this);
|
||||
this.workspace = Blockly.inject('blocklyDiv', {});
|
||||
this.injectionDiv = this.workspace.getInjectionDiv();
|
||||
Blockly.ContextMenuRegistry.registry.reset();
|
||||
Blockly.ContextMenuItems.registerDefaultOptions();
|
||||
});
|
||||
teardown(function () {
|
||||
sharedTestTeardown.call(this);
|
||||
@@ -158,87 +160,42 @@ suite('Keyboard Shortcut Items', function () {
|
||||
'hideChaff',
|
||||
);
|
||||
});
|
||||
const testCases = [
|
||||
[
|
||||
'Control C',
|
||||
createKeyDownEvent(Blockly.utils.KeyCodes.C, [
|
||||
Blockly.utils.KeyCodes.CTRL,
|
||||
]),
|
||||
],
|
||||
[
|
||||
'Meta C',
|
||||
createKeyDownEvent(Blockly.utils.KeyCodes.C, [
|
||||
Blockly.utils.KeyCodes.META,
|
||||
]),
|
||||
],
|
||||
];
|
||||
const keyEvent = createKeyDownEvent(Blockly.utils.KeyCodes.C, [
|
||||
Blockly.utils.KeyCodes.CTRL_CMD,
|
||||
]);
|
||||
// Copy a block.
|
||||
suite('Simple', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
test(testCaseName, function () {
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.calledOnce(this.copySpy);
|
||||
sinon.assert.calledOnce(this.hideChaffSpy);
|
||||
});
|
||||
});
|
||||
test('Simple', function () {
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.calledOnce(this.copySpy);
|
||||
sinon.assert.calledOnce(this.hideChaffSpy);
|
||||
});
|
||||
// Allow copying a block if a workspace is in readonly mode.
|
||||
suite('Called when readOnly is true', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
test(testCaseName, function () {
|
||||
this.workspace.setIsReadOnly(true);
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.calledOnce(this.copySpy);
|
||||
sinon.assert.calledOnce(this.hideChaffSpy);
|
||||
});
|
||||
});
|
||||
test('Called when readOnly is true', function () {
|
||||
this.workspace.setIsReadOnly(true);
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.calledOnce(this.copySpy);
|
||||
sinon.assert.calledOnce(this.hideChaffSpy);
|
||||
});
|
||||
// Do not copy a block if a drag is in progress.
|
||||
suite('Drag in progress', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
test(testCaseName, function () {
|
||||
sinon.stub(this.workspace, 'isDragging').returns(true);
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.notCalled(this.copySpy);
|
||||
sinon.assert.notCalled(this.hideChaffSpy);
|
||||
});
|
||||
});
|
||||
test('Drag in progress', function () {
|
||||
sinon.stub(this.workspace, 'isDragging').returns(true);
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.notCalled(this.copySpy);
|
||||
sinon.assert.notCalled(this.hideChaffSpy);
|
||||
});
|
||||
// Do not copy a block if is is not deletable.
|
||||
suite('Block is not deletable', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
test(testCaseName, function () {
|
||||
sinon
|
||||
.stub(Blockly.common.getSelected(), 'isOwnDeletable')
|
||||
.returns(false);
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.notCalled(this.copySpy);
|
||||
sinon.assert.notCalled(this.hideChaffSpy);
|
||||
});
|
||||
});
|
||||
test('Block is not deletable', function () {
|
||||
sinon.stub(Blockly.common.getSelected(), 'isOwnDeletable').returns(false);
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.notCalled(this.copySpy);
|
||||
sinon.assert.notCalled(this.hideChaffSpy);
|
||||
});
|
||||
// Do not copy a block if it is not movable.
|
||||
suite('Block is not movable', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
test(testCaseName, function () {
|
||||
sinon
|
||||
.stub(Blockly.common.getSelected(), 'isOwnMovable')
|
||||
.returns(false);
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.notCalled(this.copySpy);
|
||||
sinon.assert.notCalled(this.hideChaffSpy);
|
||||
});
|
||||
});
|
||||
test('Block is not movable', function () {
|
||||
sinon.stub(Blockly.common.getSelected(), 'isOwnMovable').returns(false);
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.notCalled(this.copySpy);
|
||||
sinon.assert.notCalled(this.hideChaffSpy);
|
||||
});
|
||||
test('Not called when connection is focused', function () {
|
||||
// Restore the stub behavior called during setup
|
||||
@@ -254,19 +211,13 @@ suite('Keyboard Shortcut Items', function () {
|
||||
});
|
||||
// Copy a comment.
|
||||
test('Workspace comment', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
test(testCaseName, function () {
|
||||
Blockly.getFocusManager().getFocusedNode.restore();
|
||||
this.comment = setSelectedComment(this.workspace);
|
||||
this.copySpy = sinon.spy(this.comment, 'toCopyData');
|
||||
Blockly.getFocusManager().getFocusedNode.restore();
|
||||
this.comment = setSelectedComment(this.workspace);
|
||||
this.copySpy = sinon.spy(this.comment, 'toCopyData');
|
||||
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.calledOnce(this.copySpy);
|
||||
sinon.assert.calledOnce(this.hideChaffSpy);
|
||||
});
|
||||
});
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.calledOnce(this.copySpy);
|
||||
sinon.assert.calledOnce(this.hideChaffSpy);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -280,92 +231,47 @@ suite('Keyboard Shortcut Items', function () {
|
||||
'hideChaff',
|
||||
);
|
||||
});
|
||||
const testCases = [
|
||||
[
|
||||
'Control X',
|
||||
createKeyDownEvent(Blockly.utils.KeyCodes.X, [
|
||||
Blockly.utils.KeyCodes.CTRL,
|
||||
]),
|
||||
],
|
||||
[
|
||||
'Meta X',
|
||||
createKeyDownEvent(Blockly.utils.KeyCodes.X, [
|
||||
Blockly.utils.KeyCodes.META,
|
||||
]),
|
||||
],
|
||||
];
|
||||
const keyEvent = createKeyDownEvent(Blockly.utils.KeyCodes.X, [
|
||||
Blockly.utils.KeyCodes.CTRL_CMD,
|
||||
]);
|
||||
// Cut a block.
|
||||
suite('Simple', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
test(testCaseName, function () {
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.calledOnce(this.copySpy);
|
||||
sinon.assert.calledOnce(this.disposeSpy);
|
||||
sinon.assert.calledOnce(this.hideChaffSpy);
|
||||
});
|
||||
});
|
||||
test('Simple', function () {
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.calledOnce(this.copySpy);
|
||||
sinon.assert.calledOnce(this.disposeSpy);
|
||||
sinon.assert.calledOnce(this.hideChaffSpy);
|
||||
});
|
||||
// Do not cut a block if a workspace is in readonly mode.
|
||||
suite('Not called when readOnly is true', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
test(testCaseName, function () {
|
||||
this.workspace.setIsReadOnly(true);
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.notCalled(this.copySpy);
|
||||
sinon.assert.notCalled(this.disposeSpy);
|
||||
sinon.assert.notCalled(this.hideChaffSpy);
|
||||
});
|
||||
});
|
||||
test('Not called when readOnly is true', function () {
|
||||
this.workspace.setIsReadOnly(true);
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.notCalled(this.copySpy);
|
||||
sinon.assert.notCalled(this.disposeSpy);
|
||||
sinon.assert.notCalled(this.hideChaffSpy);
|
||||
});
|
||||
// Do not cut a block if a drag is in progress.
|
||||
suite('Drag in progress', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
test(testCaseName, function () {
|
||||
sinon.stub(this.workspace, 'isDragging').returns(true);
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.notCalled(this.copySpy);
|
||||
sinon.assert.notCalled(this.disposeSpy);
|
||||
sinon.assert.notCalled(this.hideChaffSpy);
|
||||
});
|
||||
});
|
||||
test('Drag in progress', function () {
|
||||
sinon.stub(this.workspace, 'isDragging').returns(true);
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.notCalled(this.copySpy);
|
||||
sinon.assert.notCalled(this.disposeSpy);
|
||||
sinon.assert.notCalled(this.hideChaffSpy);
|
||||
});
|
||||
// Do not cut a block if is is not deletable.
|
||||
suite('Block is not deletable', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
test(testCaseName, function () {
|
||||
sinon
|
||||
.stub(Blockly.common.getSelected(), 'isOwnDeletable')
|
||||
.returns(false);
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.notCalled(this.copySpy);
|
||||
sinon.assert.notCalled(this.disposeSpy);
|
||||
sinon.assert.notCalled(this.hideChaffSpy);
|
||||
});
|
||||
});
|
||||
test('Block is not deletable', function () {
|
||||
sinon.stub(Blockly.common.getSelected(), 'isOwnDeletable').returns(false);
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.notCalled(this.copySpy);
|
||||
sinon.assert.notCalled(this.disposeSpy);
|
||||
sinon.assert.notCalled(this.hideChaffSpy);
|
||||
});
|
||||
// Do not cut a block if it is not movable.
|
||||
suite('Block is not movable', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
test(testCaseName, function () {
|
||||
sinon
|
||||
.stub(Blockly.common.getSelected(), 'isOwnMovable')
|
||||
.returns(false);
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.notCalled(this.copySpy);
|
||||
sinon.assert.notCalled(this.disposeSpy);
|
||||
sinon.assert.notCalled(this.hideChaffSpy);
|
||||
});
|
||||
});
|
||||
test('Block is not movable', function () {
|
||||
sinon.stub(Blockly.common.getSelected(), 'isOwnMovable').returns(false);
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.notCalled(this.copySpy);
|
||||
sinon.assert.notCalled(this.disposeSpy);
|
||||
sinon.assert.notCalled(this.hideChaffSpy);
|
||||
});
|
||||
test('Not called when connection is focused', function () {
|
||||
// Restore the stub behavior called during setup
|
||||
@@ -382,21 +288,15 @@ suite('Keyboard Shortcut Items', function () {
|
||||
});
|
||||
|
||||
// Cut a comment.
|
||||
suite('Workspace comment', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
test(testCaseName, function () {
|
||||
Blockly.getFocusManager().getFocusedNode.restore();
|
||||
this.comment = setSelectedComment(this.workspace);
|
||||
this.copySpy = sinon.spy(this.comment, 'toCopyData');
|
||||
this.disposeSpy = sinon.spy(this.comment, 'dispose');
|
||||
test('Workspace comment', function () {
|
||||
Blockly.getFocusManager().getFocusedNode.restore();
|
||||
this.comment = setSelectedComment(this.workspace);
|
||||
this.copySpy = sinon.spy(this.comment, 'toCopyData');
|
||||
this.disposeSpy = sinon.spy(this.comment, 'dispose');
|
||||
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.calledOnce(this.copySpy);
|
||||
sinon.assert.calledOnce(this.disposeSpy);
|
||||
});
|
||||
});
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.calledOnce(this.copySpy);
|
||||
sinon.assert.calledOnce(this.disposeSpy);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -421,53 +321,26 @@ suite('Keyboard Shortcut Items', function () {
|
||||
'hideChaff',
|
||||
);
|
||||
});
|
||||
const testCases = [
|
||||
[
|
||||
'Control Z',
|
||||
createKeyDownEvent(Blockly.utils.KeyCodes.Z, [
|
||||
Blockly.utils.KeyCodes.CTRL,
|
||||
]),
|
||||
],
|
||||
[
|
||||
'Meta Z',
|
||||
createKeyDownEvent(Blockly.utils.KeyCodes.Z, [
|
||||
Blockly.utils.KeyCodes.META,
|
||||
]),
|
||||
],
|
||||
];
|
||||
const keyEvent = createKeyDownEvent(Blockly.utils.KeyCodes.Z, [
|
||||
Blockly.utils.KeyCodes.CTRL_CMD,
|
||||
]);
|
||||
// Undo.
|
||||
suite('Simple', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
test(testCaseName, function () {
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.calledOnce(this.undoSpy);
|
||||
sinon.assert.calledWith(this.undoSpy, false);
|
||||
sinon.assert.calledOnce(this.hideChaffSpy);
|
||||
});
|
||||
});
|
||||
test('Simple', function () {
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.calledOnce(this.undoSpy);
|
||||
sinon.assert.calledWith(this.undoSpy, false);
|
||||
sinon.assert.calledOnce(this.hideChaffSpy);
|
||||
});
|
||||
// Do not undo if a drag is in progress.
|
||||
suite('Drag in progress', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
test(testCaseName, function () {
|
||||
sinon.stub(this.workspace, 'isDragging').returns(true);
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.notCalled(this.undoSpy);
|
||||
sinon.assert.notCalled(this.hideChaffSpy);
|
||||
});
|
||||
});
|
||||
test('Drag in progress', function () {
|
||||
sinon.stub(this.workspace, 'isDragging').returns(true);
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.notCalled(this.undoSpy);
|
||||
sinon.assert.notCalled(this.hideChaffSpy);
|
||||
});
|
||||
// Do not undo if the workspace is in readOnly mode.
|
||||
suite('Not called when readOnly is true', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
runReadOnlyTest(keyEvent, testCaseName);
|
||||
});
|
||||
test('Not called when readOnly is true', function () {
|
||||
runReadOnlyTest(keyEvent);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -479,55 +352,27 @@ suite('Keyboard Shortcut Items', function () {
|
||||
'hideChaff',
|
||||
);
|
||||
});
|
||||
const testCases = [
|
||||
[
|
||||
'Control Shift Z',
|
||||
createKeyDownEvent(Blockly.utils.KeyCodes.Z, [
|
||||
Blockly.utils.KeyCodes.CTRL,
|
||||
Blockly.utils.KeyCodes.SHIFT,
|
||||
]),
|
||||
],
|
||||
[
|
||||
'Meta Shift Z',
|
||||
createKeyDownEvent(Blockly.utils.KeyCodes.Z, [
|
||||
Blockly.utils.KeyCodes.META,
|
||||
Blockly.utils.KeyCodes.SHIFT,
|
||||
]),
|
||||
],
|
||||
];
|
||||
const keyEvent = createKeyDownEvent(Blockly.utils.KeyCodes.Z, [
|
||||
Blockly.utils.KeyCodes.CTRL_CMD,
|
||||
Blockly.utils.KeyCodes.SHIFT,
|
||||
]);
|
||||
// Undo.
|
||||
suite('Simple', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
test(testCaseName, function () {
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.calledOnce(this.redoSpy);
|
||||
sinon.assert.calledWith(this.redoSpy, true);
|
||||
sinon.assert.calledOnce(this.hideChaffSpy);
|
||||
});
|
||||
});
|
||||
test('Simple', function () {
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.calledOnce(this.redoSpy);
|
||||
sinon.assert.calledWith(this.redoSpy, true);
|
||||
sinon.assert.calledOnce(this.hideChaffSpy);
|
||||
});
|
||||
// Do not redo if a drag is in progress.
|
||||
suite('Drag in progress', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
test(testCaseName, function () {
|
||||
sinon.stub(this.workspace, 'isDragging').returns(true);
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.notCalled(this.redoSpy);
|
||||
sinon.assert.notCalled(this.hideChaffSpy);
|
||||
});
|
||||
});
|
||||
test('Drag in progress', function () {
|
||||
sinon.stub(this.workspace, 'isDragging').returns(true);
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.notCalled(this.redoSpy);
|
||||
sinon.assert.notCalled(this.hideChaffSpy);
|
||||
});
|
||||
// Do not undo if the workspace is in readOnly mode.
|
||||
suite('Not called when readOnly is true', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
runReadOnlyTest(keyEvent, testCaseName);
|
||||
});
|
||||
test('Not called when readOnly is true', function () {
|
||||
runReadOnlyTest(keyEvent);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -560,4 +405,85 @@ suite('Keyboard Shortcut Items', function () {
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
suite('Show context menu (Ctrl/Cmd+Enter)', function () {
|
||||
const contextMenuKeyEvent = createKeyDownEvent(
|
||||
Blockly.utils.KeyCodes.ENTER,
|
||||
[Blockly.utils.KeyCodes.CTRL_CMD],
|
||||
);
|
||||
|
||||
test('Displays context menu on a block using the keyboard shortcut', function () {
|
||||
const block = setSelectedBlock(this.workspace);
|
||||
this.injectionDiv.dispatchEvent(contextMenuKeyEvent);
|
||||
|
||||
const menu = Blockly.ContextMenu.getMenu();
|
||||
assert.instanceOf(menu, Blockly.Menu, 'Context menu should be shown');
|
||||
|
||||
const menuOptions =
|
||||
Blockly.ContextMenuRegistry.registry.getContextMenuOptions(
|
||||
{block, focusedNode: block},
|
||||
contextMenuKeyEvent,
|
||||
);
|
||||
for (const option of menuOptions) {
|
||||
assert.include(menu.getElement().innerText, option.text);
|
||||
}
|
||||
});
|
||||
|
||||
test('Displays context menu on the workspace using the keyboard shortcut', function () {
|
||||
Blockly.getFocusManager().focusNode(this.workspace);
|
||||
this.injectionDiv.dispatchEvent(contextMenuKeyEvent);
|
||||
|
||||
const menu = Blockly.ContextMenu.getMenu();
|
||||
assert.instanceOf(menu, Blockly.Menu, 'Context menu should be shown');
|
||||
const menuOptions =
|
||||
Blockly.ContextMenuRegistry.registry.getContextMenuOptions(
|
||||
{workspace: this.workspace, focusedNode: this.workspace},
|
||||
contextMenuKeyEvent,
|
||||
);
|
||||
for (const option of menuOptions) {
|
||||
assert.include(menu.getElement().innerText, option.text);
|
||||
}
|
||||
});
|
||||
|
||||
test('Displays context menu on a workspace comment using the keyboard shortcut', function () {
|
||||
Blockly.ContextMenuItems.registerCommentOptions();
|
||||
const comment = setSelectedComment(this.workspace);
|
||||
this.injectionDiv.dispatchEvent(contextMenuKeyEvent);
|
||||
|
||||
const menu = Blockly.ContextMenu.getMenu();
|
||||
assert.instanceOf(menu, Blockly.Menu, 'Context menu should be shown');
|
||||
const menuOptions =
|
||||
Blockly.ContextMenuRegistry.registry.getContextMenuOptions(
|
||||
{comment, focusedNode: comment},
|
||||
contextMenuKeyEvent,
|
||||
);
|
||||
for (const option of menuOptions) {
|
||||
assert.include(menu.getElement().innerText, option.text);
|
||||
}
|
||||
});
|
||||
|
||||
test('First menu item is highlighted when context menu is shown via keyboard shortcut', function () {
|
||||
setSelectedBlock(this.workspace);
|
||||
this.injectionDiv.dispatchEvent(contextMenuKeyEvent);
|
||||
|
||||
const menuEl = Blockly.ContextMenu.getMenu().getElement();
|
||||
const firstMenuItem = menuEl.querySelector('.blocklyMenuItem');
|
||||
assert.isTrue(
|
||||
firstMenuItem.classList.contains('blocklyMenuItemHighlight'),
|
||||
);
|
||||
});
|
||||
|
||||
test('Context menu is not shown when shortcut is invoked while a field is focused', function () {
|
||||
const block = this.workspace.newBlock('math_arithmetic');
|
||||
block.initSvg();
|
||||
const field = block.getField('OP');
|
||||
Blockly.getFocusManager().focusNode(field);
|
||||
this.injectionDiv.dispatchEvent(contextMenuKeyEvent);
|
||||
|
||||
assert.isNull(
|
||||
Blockly.ContextMenu.getMenu(),
|
||||
'Context menu should not be triggered when a field is focused',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user