diff --git a/blocks/blocks.ts b/blocks/blocks.ts index 1a35c0130..f4a9936ed 100644 --- a/blocks/blocks.ts +++ b/blocks/blocks.ts @@ -20,6 +20,7 @@ import type {BlockDefinition} from '../core/blocks.js'; export { colour, lists, + logic, loops, math, procedures, @@ -40,6 +41,7 @@ export const blocks: {[key: string]: BlockDefinition} = Object.assign( loops.blocks, math.blocks, procedures.blocks, + texts.blocks, variables.blocks, variablesDynamic.blocks, ); diff --git a/core/block_dragger.ts b/core/block_dragger.ts index 8206200a4..596f27f83 100644 --- a/core/block_dragger.ts +++ b/core/block_dragger.ts @@ -69,8 +69,18 @@ export class BlockDragger implements IBlockDragger { /** Whether the block would be deleted if dropped immediately. */ protected wouldDeleteBlock_ = false; + protected startXY_: Coordinate; + /** The parent block at the start of the drag. */ + private startParentConn: RenderedConnection | null = null; + + /** + * The child block at the start of the drag. Only gets set if + * `healStack` is true. + */ + private startChildConn: RenderedConnection | null = null; + /** * @param block The block to drag. * @param workspace The workspace to drag on. @@ -126,6 +136,13 @@ export class BlockDragger implements IBlockDragger { blockAnimation.disconnectUiStop(); if (this.shouldDisconnect_(healStack)) { + this.startParentConn = + this.draggingBlock_.outputConnection?.targetConnection ?? + this.draggingBlock_.previousConnection?.targetConnection; + if (healStack) { + this.startChildConn = + this.draggingBlock_.nextConnection?.targetConnection; + } this.disconnectBlock_(healStack, currentDragDeltaXY); } this.draggingBlock_.setDragging(true); @@ -413,17 +430,10 @@ export class BlockDragger implements IBlockDragger { .getLayerManager() ?.moveOffDragLayer(this.draggingBlock_, layers.BLOCK); this.draggingBlock_.setDragging(false); - if (delta) { - // !preventMove + if (preventMove) { + this.moveToOriginalPosition(); + } else if (delta) { this.updateBlockAfterMove_(); - } else { - // Blocks dragged directly from a flyout may need to be bumped into - // bounds. - bumpObjects.bumpIntoBounds( - this.draggingBlock_.workspace, - this.workspace_.getMetricsManager().getScrollMetrics(true), - this.draggingBlock_, - ); } } // Must dispose after `updateBlockAfterMove_` is called to not break the @@ -434,6 +444,32 @@ export class BlockDragger implements IBlockDragger { eventUtils.setGroup(false); } + /** + * Moves the dragged block back to its original position before the start of + * the drag. Reconnects any parent and child blocks. + */ + private moveToOriginalPosition() { + this.startChildConn?.connect(this.draggingBlock_.nextConnection); + if (this.startParentConn) { + switch (this.startParentConn.type) { + case ConnectionType.INPUT_VALUE: + this.startParentConn.connect(this.draggingBlock_.outputConnection); + break; + case ConnectionType.NEXT_STATEMENT: + this.startParentConn.connect(this.draggingBlock_.previousConnection); + } + } else { + this.draggingBlock_.moveTo(this.startXY_, ['drag']); + // Blocks dragged directly from a flyout may need to be bumped into + // bounds. + bumpObjects.bumpIntoBounds( + this.draggingBlock_.workspace, + this.workspace_.getMetricsManager().getScrollMetrics(true), + this.draggingBlock_, + ); + } + } + /** * Calculates the drag delta and new location values after a block is dragged. * diff --git a/core/common.ts b/core/common.ts index 29fb7777b..625921350 100644 --- a/core/common.ts +++ b/core/common.ts @@ -282,7 +282,9 @@ export function defineBlocks(blocks: {[key: string]: BlockDefinition}) { for (const type of Object.keys(blocks)) { const definition = blocks[type]; if (type in Blocks) { - console.warn(`Block definiton "${type}" overwrites previous definition.`); + console.warn( + `Block definition "${type}" overwrites previous definition.`, + ); } Blocks[type] = definition; } diff --git a/core/grid.ts b/core/grid.ts index 28e460fa0..e495e220f 100644 --- a/core/grid.ts +++ b/core/grid.ts @@ -66,12 +66,26 @@ export class Grid { this.update(this.scale); } + /** + * Get the spacing of the grid points (in px). + * + * @returns The spacing of the grid points. + */ + getSpacing(): number { + return this.spacing; + } + /** Sets the length of the grid lines. */ setLength(length: number) { this.length = length; this.update(this.scale); } + /** Get the length of the grid lines (in px). */ + getLength(): number { + return this.length; + } + /** * Sets whether blocks should snap to the grid or not. * @@ -85,25 +99,14 @@ export class Grid { } /** - * Whether blocks should snap to the grid, based on the initial configuration. + * Whether blocks should snap to the grid. * * @returns True if blocks should snap, false otherwise. - * @internal */ shouldSnap(): boolean { return this.snapToGrid; } - /** - * Get the spacing of the grid points (in px). - * - * @returns The spacing of the grid points. - * @internal - */ - getSpacing(): number { - return this.spacing; - } - /** * Get the ID of the pattern element, which should be randomized to avoid * conflicts with other Blockly instances on the page. diff --git a/core/insertion_marker_previewer.ts b/core/insertion_marker_previewer.ts index 88c63a65c..8f3be0617 100644 --- a/core/insertion_marker_previewer.ts +++ b/core/insertion_marker_previewer.ts @@ -12,6 +12,8 @@ import * as blocks from './serialization/blocks.js'; import * as eventUtils from './events/utils.js'; import * as renderManagement from './render_management.js'; import * as registry from './registry.js'; +import {Renderer as ZelosRenderer} from './renderers/zelos/renderer.js'; +import {ConnectionType} from './connection_type.js'; /** * An error message to throw if the block created by createMarkerBlock_ is @@ -97,34 +99,18 @@ export class InsertionMarkerPreviewer implements IConnectionPreviewer { throw Error(DUPLICATE_BLOCK_ERROR); } - // Render disconnected from everything else so that we have a valid - // connection location. - marker.queueRender(); - renderManagement.triggerQueuedRenders(); - - // Connect() also renders the insertion marker. - markerConn.connect(staticConn); - - const originalOffsetToTarget = { - x: staticConn.x - markerConn.x, - y: staticConn.y - markerConn.y, - }; - const originalOffsetInBlock = markerConn.getOffsetInBlock().clone(); - renderManagement.finishQueuedRenders().then(() => { - // Position so that the existing block doesn't move. - marker?.positionNearConnection( - markerConn, - originalOffsetToTarget, - originalOffsetInBlock, - ); - marker?.getSvgRoot().setAttribute('visibility', 'visible'); - }); + // TODO(7898): Instead of special casing, we should change the dragger to + // track the change in distance between the dragged connection and the + // static connection, so that it doesn't disconnect unless that + // (+ a bit) has been exceeded. + if (this.shouldUseMarkerPreview(draggedConn, staticConn)) { + this.markerConn = this.previewMarker(draggedConn, staticConn); + } if (this.workspace.getRenderer().shouldHighlightConnection(staticConn)) { staticConn.highlight(); } - this.markerConn = markerConn; this.draggedConn = draggedConn; this.staticConn = staticConn; } finally { @@ -132,6 +118,53 @@ export class InsertionMarkerPreviewer implements IConnectionPreviewer { } } + private shouldUseMarkerPreview( + _draggedConn: RenderedConnection, + staticConn: RenderedConnection, + ): boolean { + return ( + staticConn.type === ConnectionType.PREVIOUS_STATEMENT || + staticConn.type === ConnectionType.NEXT_STATEMENT || + !(this.workspace.getRenderer() instanceof ZelosRenderer) + ); + } + + private previewMarker( + draggedConn: RenderedConnection, + staticConn: RenderedConnection, + ): RenderedConnection { + const dragged = draggedConn.getSourceBlock(); + const marker = this.createInsertionMarker(dragged); + const markerConn = this.getMatchingConnection(dragged, marker, draggedConn); + if (!markerConn) { + throw Error('Could not create insertion marker to preview connection'); + } + + // Render disconnected from everything else so that we have a valid + // connection location. + marker.queueRender(); + renderManagement.triggerQueuedRenders(); + + // Connect() also renders the insertion marker. + markerConn.connect(staticConn); + + const originalOffsetToTarget = { + x: staticConn.x - markerConn.x, + y: staticConn.y - markerConn.y, + }; + const originalOffsetInBlock = markerConn.getOffsetInBlock().clone(); + renderManagement.finishQueuedRenders().then(() => { + // Position so that the existing block doesn't move. + marker?.positionNearConnection( + markerConn, + originalOffsetToTarget, + originalOffsetInBlock, + ); + marker?.getSvgRoot().setAttribute('visibility', 'visible'); + }); + return markerConn; + } + private createInsertionMarker(origBlock: BlockSvg) { const blockJson = blocks.save(origBlock, { addCoordinates: false, diff --git a/package-lock.json b/package-lock.json index c134b5caf..8b87ef3f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -236,9 +236,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -351,13 +351,13 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", "minimatch": "^3.0.5" }, "engines": { @@ -378,9 +378,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, "node_modules/@hyperjump/json-pointer": { @@ -3951,16 +3951,16 @@ } }, "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0",