diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..176a458f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/core/field_input.ts b/core/field_input.ts index 4f36ac92d..85431cc5b 100644 --- a/core/field_input.ts +++ b/core/field_input.ts @@ -395,7 +395,7 @@ export abstract class FieldInput 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(); diff --git a/core/flyout_base.ts b/core/flyout_base.ts index f9e6545a9..18f84480c 100644 --- a/core/flyout_base.ts +++ b/core/flyout_base.ts @@ -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? * diff --git a/core/flyout_button.ts b/core/flyout_button.ts index 5b20d24da..e73403d77 100644 --- a/core/flyout_button.ts +++ b/core/flyout_button.ts @@ -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); diff --git a/core/flyout_horizontal.ts b/core/flyout_horizontal.ts index 14c9b00b9..6e77636e8 100644 --- a/core/flyout_horizontal.ts +++ b/core/flyout_horizontal.ts @@ -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; diff --git a/core/flyout_vertical.ts b/core/flyout_vertical.ts index 6aa6e3370..59682a390 100644 --- a/core/flyout_vertical.ts +++ b/core/flyout_vertical.ts @@ -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; diff --git a/core/shortcut_items.ts b/core/shortcut_items.ts index b5abf5554..6a7e29d26 100644 --- a/core/shortcut_items.ts +++ b/core/shortcut_items.ts @@ -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], }; diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 96d4b19a9..aad748105 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -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);