release: Merge branch 'develop' into rc/v11.1.0

This commit is contained in:
Beka Westberg
2024-05-30 18:38:40 +00:00
21 changed files with 244 additions and 92 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto

View File

@@ -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.

View File

@@ -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);
}
}

View File

@@ -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. */

View File

@@ -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;

View File

@@ -307,6 +307,7 @@ let content = `
.blocklyMinimalBody {
margin: 0;
padding: 0;
height: 100%;
}
.blocklyHtmlInput {

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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?
*

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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],
};

View File

@@ -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.

View File

@@ -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);

View File

@@ -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"

View File

@@ -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};
`);
}));
}

View File

@@ -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()

View File

@@ -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,