mirror of
https://github.com/google/blockly.git
synced 2026-01-20 07:17:10 +01:00
release: Merge branch 'develop' into rc/v11.1.0
This commit is contained in:
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text=auto
|
||||
@@ -675,17 +675,6 @@ export class Block implements IASTNodeLocation {
|
||||
return block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this block if it is a shadow block, or the first non-shadow parent.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
getFirstNonShadowBlock(): this {
|
||||
if (!this.isShadow()) return this;
|
||||
// We can assert the parent is non-null because shadows must have parents.
|
||||
return this.getParent()!.getFirstNonShadowBlock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all the blocks that are directly nested inside this one.
|
||||
* Includes value and statement inputs, as well as any following statement.
|
||||
|
||||
@@ -599,7 +599,7 @@ export class BlockSvg
|
||||
const menuOptions = this.generateContextMenu();
|
||||
|
||||
if (menuOptions && menuOptions.length) {
|
||||
ContextMenu.show(e, menuOptions, this.RTL);
|
||||
ContextMenu.show(e, menuOptions, this.RTL, this.workspace);
|
||||
ContextMenu.setCurrentBlock(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@ export class RenderedWorkspaceComment
|
||||
ContextMenuRegistry.ScopeType.COMMENT,
|
||||
{comment: this},
|
||||
);
|
||||
contextMenu.show(e, menuOptions, this.workspace.RTL);
|
||||
contextMenu.show(e, menuOptions, this.workspace.RTL, this.workspace);
|
||||
}
|
||||
|
||||
/** Snap this comment to the nearest grid point. */
|
||||
|
||||
@@ -23,6 +23,7 @@ import {Rect} from './utils/rect.js';
|
||||
import * as serializationBlocks from './serialization/blocks.js';
|
||||
import * as svgMath from './utils/svg_math.js';
|
||||
import * as WidgetDiv from './widgetdiv.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
import * as Xml from './xml.js';
|
||||
import * as common from './common.js';
|
||||
|
||||
@@ -62,13 +63,15 @@ let menu_: Menu | null = null;
|
||||
* @param e Mouse event.
|
||||
* @param options Array of menu options.
|
||||
* @param rtl True if RTL, false if LTR.
|
||||
* @param workspace The workspace associated with the context menu, if any.
|
||||
*/
|
||||
export function show(
|
||||
e: PointerEvent,
|
||||
options: (ContextMenuOption | LegacyContextMenuOption)[],
|
||||
rtl: boolean,
|
||||
workspace?: WorkspaceSvg,
|
||||
) {
|
||||
WidgetDiv.show(dummyOwner, rtl, dispose);
|
||||
WidgetDiv.show(dummyOwner, rtl, dispose, workspace);
|
||||
if (!options.length) {
|
||||
hide();
|
||||
return;
|
||||
|
||||
@@ -307,6 +307,7 @@ let content = `
|
||||
.blocklyMinimalBody {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.blocklyHtmlInput {
|
||||
|
||||
@@ -55,15 +55,24 @@ export class BlockDragStrategy implements IDragStrategy {
|
||||
|
||||
private dragging = false;
|
||||
|
||||
/**
|
||||
* If this is a shadow block, the offset between this block and the parent
|
||||
* block, to add to the drag location. In workspace units.
|
||||
*/
|
||||
private dragOffset = new Coordinate(0, 0);
|
||||
|
||||
constructor(private block: BlockSvg) {
|
||||
this.workspace = block.workspace;
|
||||
}
|
||||
|
||||
/** Returns true if the block is currently movable. False otherwise. */
|
||||
isMovable(): boolean {
|
||||
if (this.block.isShadow()) {
|
||||
return this.block.getParent()?.isMovable() ?? false;
|
||||
}
|
||||
|
||||
return (
|
||||
this.block.isOwnMovable() &&
|
||||
!this.block.isShadow() &&
|
||||
!this.block.isDeadOrDying() &&
|
||||
!this.workspace.options.readOnly &&
|
||||
// We never drag blocks in the flyout, only create new blocks that are
|
||||
@@ -77,6 +86,11 @@ export class BlockDragStrategy implements IDragStrategy {
|
||||
* from any parent blocks.
|
||||
*/
|
||||
startDrag(e?: PointerEvent): void {
|
||||
if (this.block.isShadow()) {
|
||||
this.startDraggingShadow(e);
|
||||
return;
|
||||
}
|
||||
|
||||
this.dragging = true;
|
||||
if (!eventUtils.getGroup()) {
|
||||
eventUtils.setGroup(true);
|
||||
@@ -106,6 +120,22 @@ export class BlockDragStrategy implements IDragStrategy {
|
||||
this.workspace.getLayerManager()?.moveToDragLayer(this.block);
|
||||
}
|
||||
|
||||
/** Starts a drag on a shadow, recording the drag offset. */
|
||||
private startDraggingShadow(e?: PointerEvent) {
|
||||
const parent = this.block.getParent();
|
||||
if (!parent) {
|
||||
throw new Error(
|
||||
'Tried to drag a shadow block with no parent. ' +
|
||||
'Shadow blocks should always have parents.',
|
||||
);
|
||||
}
|
||||
this.dragOffset = Coordinate.difference(
|
||||
parent.getRelativeToSurfaceXY(),
|
||||
this.block.getRelativeToSurfaceXY(),
|
||||
);
|
||||
parent.startDrag(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not we should disconnect the block when a drag is started.
|
||||
*
|
||||
@@ -174,6 +204,11 @@ export class BlockDragStrategy implements IDragStrategy {
|
||||
|
||||
/** Moves the block and updates any connection previews. */
|
||||
drag(newLoc: Coordinate): void {
|
||||
if (this.block.isShadow()) {
|
||||
this.block.getParent()?.drag(Coordinate.sum(newLoc, this.dragOffset));
|
||||
return;
|
||||
}
|
||||
|
||||
this.block.moveDuringDrag(newLoc);
|
||||
this.updateConnectionPreview(
|
||||
this.block,
|
||||
@@ -317,7 +352,12 @@ export class BlockDragStrategy implements IDragStrategy {
|
||||
* Cleans up any state at the end of the drag. Applies any pending
|
||||
* connections.
|
||||
*/
|
||||
endDrag(): void {
|
||||
endDrag(e?: PointerEvent): void {
|
||||
if (this.block.isShadow()) {
|
||||
this.block.getParent()?.endDrag(e);
|
||||
return;
|
||||
}
|
||||
|
||||
this.fireDragEndEvent();
|
||||
this.fireMoveEvent();
|
||||
|
||||
@@ -373,6 +413,11 @@ export class BlockDragStrategy implements IDragStrategy {
|
||||
* including reconnecting connections.
|
||||
*/
|
||||
revertDrag(): void {
|
||||
if (this.block.isShadow()) {
|
||||
this.block.getParent()?.revertDrag();
|
||||
return;
|
||||
}
|
||||
|
||||
this.startChildConn?.connect(this.block.nextConnection);
|
||||
if (this.startParentConn) {
|
||||
switch (this.startParentConn.type) {
|
||||
|
||||
@@ -42,13 +42,12 @@ export class Dragger implements IDragger {
|
||||
*/
|
||||
onDrag(e: PointerEvent, totalDelta: Coordinate) {
|
||||
this.moveDraggable(e, totalDelta);
|
||||
const root = this.getRoot(this.draggable);
|
||||
|
||||
// Must check `wouldDelete` before calling other hooks on drag targets
|
||||
// since we have documented that we would do so.
|
||||
if (isDeletable(this.draggable)) {
|
||||
this.draggable.setDeleteStyle(
|
||||
this.wouldDeleteDraggable(e, this.draggable),
|
||||
);
|
||||
if (isDeletable(root)) {
|
||||
root.setDeleteStyle(this.wouldDeleteDraggable(e, root));
|
||||
}
|
||||
this.updateDragTarget(e);
|
||||
}
|
||||
@@ -56,11 +55,12 @@ export class Dragger implements IDragger {
|
||||
/** Updates the drag target under the pointer (if there is one). */
|
||||
protected updateDragTarget(e: PointerEvent) {
|
||||
const newDragTarget = this.workspace.getDragTarget(e);
|
||||
const root = this.getRoot(this.draggable);
|
||||
if (this.dragTarget !== newDragTarget) {
|
||||
this.dragTarget?.onDragExit(this.draggable);
|
||||
newDragTarget?.onDragEnter(this.draggable);
|
||||
this.dragTarget?.onDragExit(root);
|
||||
newDragTarget?.onDragEnter(root);
|
||||
}
|
||||
newDragTarget?.onDragOver(this.draggable);
|
||||
newDragTarget?.onDragOver(root);
|
||||
this.dragTarget = newDragTarget;
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ export class Dragger implements IDragger {
|
||||
*/
|
||||
protected wouldDeleteDraggable(
|
||||
e: PointerEvent,
|
||||
draggable: IDraggable & IDeletable,
|
||||
rootDraggable: IDraggable & IDeletable,
|
||||
) {
|
||||
const dragTarget = this.workspace.getDragTarget(e);
|
||||
if (!dragTarget) return false;
|
||||
@@ -92,50 +92,56 @@ export class Dragger implements IDragger {
|
||||
);
|
||||
if (!isDeleteArea) return false;
|
||||
|
||||
return (dragTarget as IDeleteArea).wouldDelete(draggable);
|
||||
return (dragTarget as IDeleteArea).wouldDelete(rootDraggable);
|
||||
}
|
||||
|
||||
/** Handles any drag cleanup. */
|
||||
onDragEnd(e: PointerEvent) {
|
||||
const origGroup = eventUtils.getGroup();
|
||||
const dragTarget = this.workspace.getDragTarget(e);
|
||||
const root = this.getRoot(this.draggable);
|
||||
|
||||
if (dragTarget) {
|
||||
this.dragTarget?.onDrop(this.draggable);
|
||||
this.dragTarget?.onDrop(root);
|
||||
}
|
||||
|
||||
if (this.shouldReturnToStart(e, this.draggable)) {
|
||||
if (this.shouldReturnToStart(e, root)) {
|
||||
this.draggable.revertDrag();
|
||||
}
|
||||
|
||||
const wouldDelete =
|
||||
isDeletable(this.draggable) &&
|
||||
this.wouldDeleteDraggable(e, this.draggable);
|
||||
const wouldDelete = isDeletable(root) && this.wouldDeleteDraggable(e, root);
|
||||
|
||||
// TODO(#8148): use a generalized API instead of an instanceof check.
|
||||
if (wouldDelete && this.draggable instanceof BlockSvg) {
|
||||
blockAnimations.disposeUiEffect(this.draggable);
|
||||
blockAnimations.disposeUiEffect(this.draggable.getRootBlock());
|
||||
}
|
||||
|
||||
this.draggable.endDrag(e);
|
||||
|
||||
if (wouldDelete && isDeletable(this.draggable)) {
|
||||
if (wouldDelete && isDeletable(root)) {
|
||||
// We want to make sure the delete gets grouped with any possible
|
||||
// move event.
|
||||
const newGroup = eventUtils.getGroup();
|
||||
eventUtils.setGroup(origGroup);
|
||||
this.draggable.dispose();
|
||||
root.dispose();
|
||||
eventUtils.setGroup(newGroup);
|
||||
}
|
||||
}
|
||||
|
||||
// We need to special case blocks for now so that we look at the root block
|
||||
// instead of the one actually being dragged in most cases.
|
||||
private getRoot(draggable: IDraggable): IDraggable {
|
||||
return draggable instanceof BlockSvg ? draggable.getRootBlock() : draggable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we should return the draggable to its original location
|
||||
* at the end of the drag.
|
||||
*/
|
||||
protected shouldReturnToStart(e: PointerEvent, draggable: IDraggable) {
|
||||
protected shouldReturnToStart(e: PointerEvent, rootDraggable: IDraggable) {
|
||||
const dragTarget = this.workspace.getDragTarget(e);
|
||||
if (!dragTarget) return false;
|
||||
return dragTarget.shouldPreventMove(draggable);
|
||||
return dragTarget.shouldPreventMove(rootDraggable);
|
||||
}
|
||||
|
||||
protected pixelsToWorkspaceUnits(pixelCoord: Coordinate): Coordinate {
|
||||
|
||||
@@ -372,7 +372,12 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
|
||||
if (!block) {
|
||||
throw new UnattachedFieldError();
|
||||
}
|
||||
WidgetDiv.show(this, block.RTL, this.widgetDispose_.bind(this));
|
||||
WidgetDiv.show(
|
||||
this,
|
||||
block.RTL,
|
||||
this.widgetDispose_.bind(this),
|
||||
this.workspace_,
|
||||
);
|
||||
this.htmlInput_ = this.widgetCreate_() as HTMLInputElement;
|
||||
this.isBeingEdited_ = true;
|
||||
this.valueWhenEditorWasOpened_ = this.value_;
|
||||
@@ -390,7 +395,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
|
||||
*
|
||||
* @returns The newly created text input editor.
|
||||
*/
|
||||
protected widgetCreate_(): HTMLElement {
|
||||
protected widgetCreate_(): HTMLInputElement | HTMLTextAreaElement {
|
||||
const block = this.getSourceBlock();
|
||||
if (!block) {
|
||||
throw new UnattachedFieldError();
|
||||
@@ -546,17 +551,17 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
|
||||
*/
|
||||
protected onHtmlInputKeyDown_(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter') {
|
||||
WidgetDiv.hide();
|
||||
WidgetDiv.hideIfOwner(this);
|
||||
dropDownDiv.hideWithoutAnimation();
|
||||
} else if (e.key === 'Escape') {
|
||||
this.setValue(
|
||||
this.htmlInput_!.getAttribute('data-untyped-default-value'),
|
||||
false,
|
||||
);
|
||||
WidgetDiv.hide();
|
||||
WidgetDiv.hideIfOwner(this);
|
||||
dropDownDiv.hideWithoutAnimation();
|
||||
} else if (e.key === 'Tab') {
|
||||
WidgetDiv.hide();
|
||||
WidgetDiv.hideIfOwner(this);
|
||||
dropDownDiv.hideWithoutAnimation();
|
||||
(this.sourceBlock_ as BlockSvg).tab(this, !e.shiftKey);
|
||||
e.preventDefault();
|
||||
|
||||
@@ -516,6 +516,15 @@ export abstract class Flyout
|
||||
this.hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the target workspace inside the flyout.
|
||||
*
|
||||
* @returns The target workspace inside the flyout.
|
||||
*/
|
||||
getTargetWorkspace(): WorkspaceSvg {
|
||||
return this.targetWorkspace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the flyout visible?
|
||||
*
|
||||
|
||||
@@ -66,14 +66,14 @@ export class FlyoutButton implements IASTNodeLocationSvg {
|
||||
* @param workspace The workspace in which to place this button.
|
||||
* @param targetWorkspace The flyout's target workspace.
|
||||
* @param json The JSON specifying the label/button.
|
||||
* @param isLabel_ Whether this button should be styled as a label.
|
||||
* @param isFlyoutLabel Whether this button should be styled as a label.
|
||||
* @internal
|
||||
*/
|
||||
constructor(
|
||||
private readonly workspace: WorkspaceSvg,
|
||||
private readonly targetWorkspace: WorkspaceSvg,
|
||||
json: toolbox.ButtonOrLabelInfo,
|
||||
private readonly isLabel_: boolean,
|
||||
private readonly isFlyoutLabel: boolean,
|
||||
) {
|
||||
this.text = json['text'];
|
||||
|
||||
@@ -100,7 +100,9 @@ export class FlyoutButton implements IASTNodeLocationSvg {
|
||||
* @returns The button's SVG group.
|
||||
*/
|
||||
createDom(): SVGElement {
|
||||
let cssClass = this.isLabel_ ? 'blocklyFlyoutLabel' : 'blocklyFlyoutButton';
|
||||
let cssClass = this.isFlyoutLabel
|
||||
? 'blocklyFlyoutLabel'
|
||||
: 'blocklyFlyoutButton';
|
||||
if (this.cssClass) {
|
||||
cssClass += ' ' + this.cssClass;
|
||||
}
|
||||
@@ -112,7 +114,7 @@ export class FlyoutButton implements IASTNodeLocationSvg {
|
||||
);
|
||||
|
||||
let shadow;
|
||||
if (!this.isLabel_) {
|
||||
if (!this.isFlyoutLabel) {
|
||||
// Shadow rectangle (light source does not mirror in RTL).
|
||||
shadow = dom.createSvgElement(
|
||||
Svg.RECT,
|
||||
@@ -130,7 +132,7 @@ export class FlyoutButton implements IASTNodeLocationSvg {
|
||||
const rect = dom.createSvgElement(
|
||||
Svg.RECT,
|
||||
{
|
||||
'class': this.isLabel_
|
||||
'class': this.isFlyoutLabel
|
||||
? 'blocklyFlyoutLabelBackground'
|
||||
: 'blocklyFlyoutButtonBackground',
|
||||
'rx': FlyoutButton.BORDER_RADIUS,
|
||||
@@ -142,7 +144,7 @@ export class FlyoutButton implements IASTNodeLocationSvg {
|
||||
const svgText = dom.createSvgElement(
|
||||
Svg.TEXT,
|
||||
{
|
||||
'class': this.isLabel_ ? 'blocklyFlyoutLabelText' : 'blocklyText',
|
||||
'class': this.isFlyoutLabel ? 'blocklyFlyoutLabelText' : 'blocklyText',
|
||||
'x': 0,
|
||||
'y': 0,
|
||||
'text-anchor': 'middle',
|
||||
@@ -155,7 +157,7 @@ export class FlyoutButton implements IASTNodeLocationSvg {
|
||||
text += '\u200F';
|
||||
}
|
||||
svgText.textContent = text;
|
||||
if (this.isLabel_) {
|
||||
if (this.isFlyoutLabel) {
|
||||
this.svgText = svgText;
|
||||
this.workspace
|
||||
.getThemeManager()
|
||||
@@ -179,7 +181,7 @@ export class FlyoutButton implements IASTNodeLocationSvg {
|
||||
);
|
||||
this.height = fontMetrics.height;
|
||||
|
||||
if (!this.isLabel_) {
|
||||
if (!this.isFlyoutLabel) {
|
||||
this.width += 2 * FlyoutButton.TEXT_MARGIN_X;
|
||||
this.height += 2 * FlyoutButton.TEXT_MARGIN_Y;
|
||||
shadow?.setAttribute('width', String(this.width));
|
||||
@@ -235,7 +237,7 @@ export class FlyoutButton implements IASTNodeLocationSvg {
|
||||
|
||||
/** @returns Whether or not the button is a label. */
|
||||
isLabel(): boolean {
|
||||
return this.isLabel_;
|
||||
return this.isFlyoutLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -321,19 +323,19 @@ export class FlyoutButton implements IASTNodeLocationSvg {
|
||||
gesture.cancel();
|
||||
}
|
||||
|
||||
if (this.isLabel_ && this.callbackKey) {
|
||||
if (this.isFlyoutLabel && this.callbackKey) {
|
||||
console.warn(
|
||||
'Labels should not have callbacks. Label text: ' + this.text,
|
||||
);
|
||||
} else if (
|
||||
!this.isLabel_ &&
|
||||
!this.isFlyoutLabel &&
|
||||
!(
|
||||
this.callbackKey &&
|
||||
this.targetWorkspace.getButtonCallback(this.callbackKey)
|
||||
)
|
||||
) {
|
||||
console.warn('Buttons should have callbacks. Button text: ' + this.text);
|
||||
} else if (!this.isLabel_) {
|
||||
} else if (!this.isFlyoutLabel) {
|
||||
const callback = this.targetWorkspace.getButtonCallback(this.callbackKey);
|
||||
if (callback) {
|
||||
callback(this);
|
||||
|
||||
@@ -240,7 +240,7 @@ export class HorizontalFlyout extends Flyout {
|
||||
this.workspace_.scrollbar?.setX(pos);
|
||||
// When the flyout moves from a wheel event, hide WidgetDiv and
|
||||
// dropDownDiv.
|
||||
WidgetDiv.hide();
|
||||
WidgetDiv.hideIfOwnerIsInWorkspace(this.workspace_);
|
||||
dropDownDiv.hideWithoutAnimation();
|
||||
}
|
||||
// Don't scroll the page.
|
||||
@@ -267,29 +267,35 @@ export class HorizontalFlyout extends Flyout {
|
||||
for (let i = 0, item; (item = contents[i]); i++) {
|
||||
if (item.type === 'block') {
|
||||
const block = item.block;
|
||||
const allBlocks = block!.getDescendants(false);
|
||||
|
||||
if (block === undefined || block === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const allBlocks = block.getDescendants(false);
|
||||
|
||||
for (let j = 0, child; (child = allBlocks[j]); j++) {
|
||||
// Mark blocks as being inside a flyout. This is used to detect and
|
||||
// prevent the closure of the flyout if the user right-clicks on such
|
||||
// a block.
|
||||
child.isInFlyout = true;
|
||||
}
|
||||
const root = block!.getSvgRoot();
|
||||
const blockHW = block!.getHeightWidth();
|
||||
const root = block.getSvgRoot();
|
||||
const blockHW = block.getHeightWidth();
|
||||
// Figure out where to place the block.
|
||||
const tab = block!.outputConnection ? this.tabWidth_ : 0;
|
||||
const tab = block.outputConnection ? this.tabWidth_ : 0;
|
||||
let moveX;
|
||||
if (this.RTL) {
|
||||
moveX = cursorX + blockHW.width;
|
||||
} else {
|
||||
moveX = cursorX - tab;
|
||||
}
|
||||
block!.moveBy(moveX, cursorY);
|
||||
block.moveBy(moveX, cursorY);
|
||||
|
||||
const rect = this.createRect_(block!, moveX, cursorY, blockHW, i);
|
||||
const rect = this.createRect_(block, moveX, cursorY, blockHW, i);
|
||||
cursorX += blockHW.width + gaps[i];
|
||||
|
||||
this.addBlockListeners_(root, block!, rect);
|
||||
this.addBlockListeners_(root, block, rect);
|
||||
} else if (item.type === 'button') {
|
||||
const button = item.button as FlyoutButton;
|
||||
this.initFlyoutButton_(button, cursorX, cursorY);
|
||||
@@ -306,7 +312,6 @@ export class HorizontalFlyout extends Flyout {
|
||||
* @param currentDragDeltaXY How far the pointer has moved from the position
|
||||
* at mouse down, in pixel units.
|
||||
* @returns True if the drag is toward the workspace.
|
||||
* @internal
|
||||
*/
|
||||
override isDragTowardWorkspace(currentDragDeltaXY: Coordinate): boolean {
|
||||
const dx = currentDragDeltaXY.x;
|
||||
|
||||
@@ -209,7 +209,7 @@ export class VerticalFlyout extends Flyout {
|
||||
this.workspace_.scrollbar?.setY(pos);
|
||||
// When the flyout moves from a wheel event, hide WidgetDiv and
|
||||
// dropDownDiv.
|
||||
WidgetDiv.hide();
|
||||
WidgetDiv.hideIfOwnerIsInWorkspace(this.workspace_);
|
||||
dropDownDiv.hideWithoutAnimation();
|
||||
}
|
||||
// Don't scroll the page.
|
||||
@@ -233,29 +233,32 @@ export class VerticalFlyout extends Flyout {
|
||||
for (let i = 0, item; (item = contents[i]); i++) {
|
||||
if (item.type === 'block') {
|
||||
const block = item.block;
|
||||
const allBlocks = block!.getDescendants(false);
|
||||
if (!block) {
|
||||
continue;
|
||||
}
|
||||
const allBlocks = block.getDescendants(false);
|
||||
for (let j = 0, child; (child = allBlocks[j]); j++) {
|
||||
// Mark blocks as being inside a flyout. This is used to detect and
|
||||
// prevent the closure of the flyout if the user right-clicks on such
|
||||
// a block.
|
||||
child.isInFlyout = true;
|
||||
}
|
||||
const root = block!.getSvgRoot();
|
||||
const blockHW = block!.getHeightWidth();
|
||||
const moveX = block!.outputConnection
|
||||
const root = block.getSvgRoot();
|
||||
const blockHW = block.getHeightWidth();
|
||||
const moveX = block.outputConnection
|
||||
? cursorX - this.tabWidth_
|
||||
: cursorX;
|
||||
block!.moveBy(moveX, cursorY);
|
||||
block.moveBy(moveX, cursorY);
|
||||
|
||||
const rect = this.createRect_(
|
||||
block!,
|
||||
block,
|
||||
this.RTL ? moveX - blockHW.width : moveX,
|
||||
cursorY,
|
||||
blockHW,
|
||||
i,
|
||||
);
|
||||
|
||||
this.addBlockListeners_(root, block!, rect);
|
||||
this.addBlockListeners_(root, block, rect);
|
||||
|
||||
cursorY += blockHW.height + gaps[i];
|
||||
} else if (item.type === 'button') {
|
||||
@@ -274,7 +277,6 @@ export class VerticalFlyout extends Flyout {
|
||||
* @param currentDragDeltaXY How far the pointer has moved from the position
|
||||
* at mouse down, in pixel units.
|
||||
* @returns True if the drag is toward the workspace.
|
||||
* @internal
|
||||
*/
|
||||
override isDragTowardWorkspace(currentDragDeltaXY: Coordinate): boolean {
|
||||
const dx = currentDragDeltaXY.x;
|
||||
|
||||
@@ -1015,7 +1015,7 @@ export class Gesture {
|
||||
// If the gesture already went through a bubble, don't set the start block.
|
||||
if (!this.startBlock && !this.startBubble) {
|
||||
this.startBlock = block;
|
||||
common.setSelected(this.startBlock.getFirstNonShadowBlock());
|
||||
common.setSelected(this.startBlock);
|
||||
if (block.isInFlyout && block !== block.getRootBlock()) {
|
||||
this.setTargetBlock(block.getRootBlock());
|
||||
} else {
|
||||
|
||||
@@ -17,6 +17,8 @@ import {KeyCodes} from './utils/keycodes.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
import {isDraggable} from './interfaces/i_draggable.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import {Coordinate} from './utils/coordinate.js';
|
||||
import {Rect} from './utils/rect.js';
|
||||
|
||||
/**
|
||||
* Object holding the names of the default shortcut items.
|
||||
@@ -63,7 +65,8 @@ export function registerDelete() {
|
||||
!workspace.options.readOnly &&
|
||||
selected != null &&
|
||||
isDeletable(selected) &&
|
||||
selected.isDeletable()
|
||||
selected.isDeletable() &&
|
||||
!Gesture.inProgress()
|
||||
);
|
||||
},
|
||||
callback(workspace, e) {
|
||||
@@ -72,10 +75,6 @@ export function registerDelete() {
|
||||
// Do this first to prevent an error in the delete code from resulting in
|
||||
// data loss.
|
||||
e.preventDefault();
|
||||
// Don't delete while dragging. Jeez.
|
||||
if (Gesture.inProgress()) {
|
||||
return false;
|
||||
}
|
||||
const selected = common.getSelected();
|
||||
if (selected instanceof BlockSvg) {
|
||||
selected.checkAndDelete();
|
||||
@@ -93,6 +92,7 @@ export function registerDelete() {
|
||||
|
||||
let copyData: ICopyData | null = null;
|
||||
let copyWorkspace: WorkspaceSvg | null = null;
|
||||
let copyCoords: Coordinate | null = null;
|
||||
|
||||
/**
|
||||
* Keyboard shortcut to copy a block on ctrl+c, cmd+c, or alt+c.
|
||||
@@ -132,6 +132,9 @@ export function registerCopy() {
|
||||
if (!selected || !isCopyable(selected)) return false;
|
||||
copyData = selected.toCopyData();
|
||||
copyWorkspace = workspace;
|
||||
copyCoords = isDraggable(selected)
|
||||
? selected.getRelativeToSurfaceXY()
|
||||
: null;
|
||||
return !!copyData;
|
||||
},
|
||||
keyCodes: [ctrlC, altC, metaC],
|
||||
@@ -174,6 +177,7 @@ export function registerCut() {
|
||||
if (selected instanceof BlockSvg) {
|
||||
copyData = selected.toCopyData();
|
||||
copyWorkspace = workspace;
|
||||
copyCoords = selected.getRelativeToSurfaceXY();
|
||||
selected.checkAndDelete();
|
||||
return true;
|
||||
} else if (
|
||||
@@ -183,6 +187,9 @@ export function registerCut() {
|
||||
) {
|
||||
copyData = selected.toCopyData();
|
||||
copyWorkspace = workspace;
|
||||
copyCoords = isDraggable(selected)
|
||||
? selected.getRelativeToSurfaceXY()
|
||||
: null;
|
||||
selected.dispose();
|
||||
return true;
|
||||
}
|
||||
@@ -215,7 +222,26 @@ export function registerPaste() {
|
||||
},
|
||||
callback() {
|
||||
if (!copyData || !copyWorkspace) return false;
|
||||
return !!clipboard.paste(copyData, copyWorkspace);
|
||||
if (!copyCoords) {
|
||||
// If we don't have location data about the original copyable, let the
|
||||
// paster determine position.
|
||||
return !!clipboard.paste(copyData, copyWorkspace);
|
||||
}
|
||||
|
||||
const {left, top, width, height} = copyWorkspace
|
||||
.getMetricsManager()
|
||||
.getViewMetrics(true);
|
||||
const viewportRect = new Rect(top, top + height, left, left + width);
|
||||
|
||||
if (viewportRect.contains(copyCoords.x, copyCoords.y)) {
|
||||
// If the original copyable is inside the viewport, let the paster
|
||||
// determine position.
|
||||
return !!clipboard.paste(copyData, copyWorkspace);
|
||||
}
|
||||
|
||||
// Otherwise, paste in the middle of the viewport.
|
||||
const centerCoords = new Coordinate(left + width / 2, top + height / 2);
|
||||
return !!clipboard.paste(copyData, copyWorkspace, centerCoords);
|
||||
},
|
||||
keyCodes: [ctrlV, altV, metaV],
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import * as common from './common.js';
|
||||
import * as dom from './utils/dom.js';
|
||||
import type {Field} from './field.js';
|
||||
import {Field} from './field.js';
|
||||
import type {Rect} from './utils/rect.js';
|
||||
import type {Size} from './utils/size.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
@@ -16,6 +16,9 @@ import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
/** The object currently using this container. */
|
||||
let owner: unknown = null;
|
||||
|
||||
/** The workspace associated with the owner currently using this container. */
|
||||
let ownerWorkspace: WorkspaceSvg | null = null;
|
||||
|
||||
/** Optional cleanup function set by whichever object uses the widget. */
|
||||
let dispose: (() => void) | null = null;
|
||||
|
||||
@@ -76,8 +79,14 @@ export function createDom() {
|
||||
* @param rtl Right-to-left (true) or left-to-right (false).
|
||||
* @param newDispose Optional cleanup function to be run when the widget is
|
||||
* closed.
|
||||
* @param workspace The workspace associated with the widget owner.
|
||||
*/
|
||||
export function show(newOwner: unknown, rtl: boolean, newDispose: () => void) {
|
||||
export function show(
|
||||
newOwner: unknown,
|
||||
rtl: boolean,
|
||||
newDispose: () => void,
|
||||
workspace?: WorkspaceSvg | null,
|
||||
) {
|
||||
hide();
|
||||
owner = newOwner;
|
||||
dispose = newDispose;
|
||||
@@ -85,9 +94,16 @@ export function show(newOwner: unknown, rtl: boolean, newDispose: () => void) {
|
||||
if (!div) return;
|
||||
div.style.direction = rtl ? 'rtl' : 'ltr';
|
||||
div.style.display = 'block';
|
||||
const mainWorkspace = common.getMainWorkspace() as WorkspaceSvg;
|
||||
rendererClassName = mainWorkspace.getRenderer().getClassName();
|
||||
themeClassName = mainWorkspace.getTheme().getClassName();
|
||||
if (!workspace && newOwner instanceof Field) {
|
||||
// For backward compatibility with plugin fields that do not provide a
|
||||
// workspace to this function, attempt to derive it from the field.
|
||||
workspace = (newOwner as Field).getSourceBlock()?.workspace as WorkspaceSvg;
|
||||
}
|
||||
ownerWorkspace = workspace ?? null;
|
||||
const rendererWorkspace =
|
||||
workspace ?? (common.getMainWorkspace() as WorkspaceSvg);
|
||||
rendererClassName = rendererWorkspace.getRenderer().getClassName();
|
||||
themeClassName = rendererWorkspace.getTheme().getClassName();
|
||||
if (rendererClassName) {
|
||||
dom.addClass(div, rendererClassName);
|
||||
}
|
||||
@@ -145,6 +161,19 @@ export function hideIfOwner(oldOwner: unknown) {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the widget and hide the div if it is being used by an object in the
|
||||
* specified workspace, or if it is used by an unknown workspace.
|
||||
*
|
||||
* @param oldOwnerWorkspace The workspace that was using this container.
|
||||
*/
|
||||
export function hideIfOwnerIsInWorkspace(oldOwnerWorkspace: WorkspaceSvg) {
|
||||
if (ownerWorkspace === null || ownerWorkspace === oldOwnerWorkspace) {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the widget div's position and height. This function does nothing clever:
|
||||
* it will not ensure that your widget div ends up in the visible window.
|
||||
|
||||
@@ -1686,7 +1686,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
|
||||
this.configureContextMenu(menuOptions, e);
|
||||
}
|
||||
|
||||
ContextMenu.show(e, menuOptions, this.RTL);
|
||||
ContextMenu.show(e, menuOptions, this.RTL, this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2039,7 +2039,6 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
|
||||
*
|
||||
* @param x Target X to scroll to.
|
||||
* @param y Target Y to scroll to.
|
||||
* @internal
|
||||
*/
|
||||
scroll(x: number, y: number) {
|
||||
this.hideChaff(/* opt_onlyClosePopups= */ true);
|
||||
@@ -2376,7 +2375,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
|
||||
*/
|
||||
hideChaff(onlyClosePopups = false) {
|
||||
Tooltip.hide();
|
||||
WidgetDiv.hide();
|
||||
WidgetDiv.hideIfOwnerIsInWorkspace(this);
|
||||
dropDownDiv.hideWithoutAnimation();
|
||||
|
||||
this.hideComponents(onlyClosePopups);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/google/blockly.git"
|
||||
"url": "git+https://github.com/google/blockly.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/google/blockly/issues"
|
||||
|
||||
@@ -677,12 +677,13 @@ async function buildLangfileShims() {
|
||||
// its named exports.
|
||||
const cjsPath = `./${lang}.js`;
|
||||
const wrapperPath = path.join(RELEASE_DIR, 'msg', `${lang}.mjs`);
|
||||
const safeLang = lang.replace(/-/g, '_');
|
||||
|
||||
await fsPromises.writeFile(wrapperPath,
|
||||
`import ${lang} from '${cjsPath}';
|
||||
`import ${safeLang} from '${cjsPath}';
|
||||
export const {
|
||||
${exportedNames.map((name) => ` ${name},`).join('\n')}
|
||||
} = ${lang};
|
||||
} = ${safeLang};
|
||||
`);
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ const {
|
||||
testFileLocations,
|
||||
dragBlockTypeFromFlyout,
|
||||
screenDirection,
|
||||
PAUSE_TIME,
|
||||
clickWorkspace,
|
||||
} = require('./test_setup');
|
||||
const {Key} = require('webdriverio');
|
||||
|
||||
@@ -48,9 +48,7 @@ async function testFieldEdits(browser, direction) {
|
||||
await browser.keys(['1093']);
|
||||
|
||||
// Click on the workspace to exit the field editor
|
||||
const workspace = await browser.$('#blocklyDiv > div > svg.blocklySvg > g');
|
||||
await workspace.click();
|
||||
await browser.pause(PAUSE_TIME);
|
||||
await clickWorkspace(browser);
|
||||
|
||||
const fieldValue = await browser.execute((id) => {
|
||||
return Blockly.getMainWorkspace()
|
||||
|
||||
@@ -198,6 +198,35 @@ async function clickBlock(browser, block, clickOptions) {
|
||||
}, findableId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the svg root of the main workspace.
|
||||
* @param browser The active WebdriverIO Browser object.
|
||||
* @return A Promise that resolves when the actions are completed.
|
||||
*/
|
||||
async function clickWorkspace(browser) {
|
||||
const workspace = await browser.$('#blocklyDiv > div > svg.blocklySvg > g');
|
||||
await workspace.click();
|
||||
await browser.pause(PAUSE_TIME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the svg root of the first mutator workspace found.
|
||||
* @param browser The active WebdriverIO Browser object.
|
||||
* @return A Promise that resolves when the actions are completed.
|
||||
* @throws If the mutator workspace cannot be found.
|
||||
*/
|
||||
async function clickMutatorWorkspace(browser) {
|
||||
const hasMutator = await browser.$('.blocklyMutatorBackground');
|
||||
if (!hasMutator) {
|
||||
throw new Error('No mutator workspace found');
|
||||
}
|
||||
const workspace = await browser
|
||||
.$('.blocklyMutatorBackground')
|
||||
.closest('g.blocklyWorkspace');
|
||||
await workspace.click();
|
||||
await browser.pause(PAUSE_TIME);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param browser The active WebdriverIO Browser object.
|
||||
* @param categoryName The name of the toolbox category to find.
|
||||
@@ -549,6 +578,8 @@ module.exports = {
|
||||
getSelectedBlockId,
|
||||
getBlockElementById,
|
||||
clickBlock,
|
||||
clickWorkspace,
|
||||
clickMutatorWorkspace,
|
||||
getCategory,
|
||||
getNthBlockOfCategory,
|
||||
getBlockTypeFromCategory,
|
||||
|
||||
Reference in New Issue
Block a user