diff --git a/appengine/app.yaml b/appengine/app.yaml index 1778f2b93..9c93fb687 100644 --- a/appengine/app.yaml +++ b/appengine/app.yaml @@ -1,4 +1,4 @@ -runtime: python37 +runtime: python312 handlers: # Redirect obsolete URLs. diff --git a/core/block_dragger.ts b/core/block_dragger.ts index 0cefc4cfb..ada0559ba 100644 --- a/core/block_dragger.ts +++ b/core/block_dragger.ts @@ -21,7 +21,6 @@ import * as common from './common.js'; import type {BlockMove} from './events/events_block_move.js'; import * as eventUtils from './events/utils.js'; import type {Icon} from './icons/icon.js'; -import {InsertionMarkerManager} from './insertion_marker_manager.js'; import type {IBlockDragger} from './interfaces/i_block_dragger.js'; import type {IDragTarget} from './interfaces/i_drag_target.js'; import * as registry from './registry.js'; @@ -29,6 +28,26 @@ import {Coordinate} from './utils/coordinate.js'; import * as dom from './utils/dom.js'; import type {WorkspaceSvg} from './workspace_svg.js'; import * as layers from './layers.js'; +import {ConnectionType, IConnectionPreviewer} from './blockly.js'; +import {RenderedConnection} from './rendered_connection.js'; +import {config} from './config.js'; +import {ComponentManager} from './component_manager.js'; +import {IDeleteArea} from './interfaces/i_delete_area.js'; +import {Connection} from './connection.js'; +import {Block} from './block.js'; +import {finishQueuedRenders} from './render_management.js'; + +/** Represents a nearby valid connection. */ +interface ConnectionCandidate { + /** A connection on the dragging stack that is compatible with neighbour. */ + local: RenderedConnection; + + /** A nearby connection that is compatible with local. */ + neighbour: RenderedConnection; + + /** The distance between the local connection and the neighbour connection. */ + distance: number; +} /** * Class for a block dragger. It moves blocks around the workspace when they @@ -37,7 +56,8 @@ import * as layers from './layers.js'; export class BlockDragger implements IBlockDragger { /** The top block in the stack that is being dragged. */ protected draggingBlock_: BlockSvg; - protected draggedConnectionManager_: InsertionMarkerManager; + + protected connectionPreviewer: IConnectionPreviewer; /** The workspace on which the block is being dragged. */ protected workspace_: WorkspaceSvg; @@ -45,6 +65,8 @@ export class BlockDragger implements IBlockDragger { /** Which drag area the mouse pointer is over, if any. */ private dragTarget_: IDragTarget | null = null; + private connectionCandidate: ConnectionCandidate | null = null; + /** Whether the block would be deleted if dropped immediately. */ protected wouldDeleteBlock_ = false; protected startXY_: Coordinate; @@ -55,14 +77,14 @@ export class BlockDragger implements IBlockDragger { */ constructor(block: BlockSvg, workspace: WorkspaceSvg) { this.draggingBlock_ = block; - - /** Object that keeps track of connections on dragged blocks. */ - this.draggedConnectionManager_ = new InsertionMarkerManager( - this.draggingBlock_, - ); - this.workspace_ = workspace; + const previewerConstructor = registry.getClassFromOptions( + registry.Type.CONNECTION_PREVIEWER, + this.workspace_.options, + ); + this.connectionPreviewer = new previewerConstructor!(block); + /** * The location of the top left corner of the dragging block at the * beginning of the drag in workspace coordinates. @@ -76,9 +98,7 @@ export class BlockDragger implements IBlockDragger { * @internal */ dispose() { - if (this.draggedConnectionManager_) { - this.draggedConnectionManager_.dispose(); - } + this.connectionPreviewer.dispose(); } /** @@ -144,7 +164,6 @@ export class BlockDragger implements IBlockDragger { this.draggingBlock_.translate(newLoc.x, newLoc.y); blockAnimation.disconnectUiEffect(this.draggingBlock_); - this.draggedConnectionManager_.updateAvailableConnections(); } /** Fire a UI event at the start of a block drag. */ @@ -162,32 +181,178 @@ export class BlockDragger implements IBlockDragger { * display accordingly. * * @param e The most recent move event. - * @param currentDragDeltaXY How far the pointer has moved from the position + * @param delta How far the pointer has moved from the position * at the start of the drag, in pixel units. */ - drag(e: PointerEvent, currentDragDeltaXY: Coordinate) { - const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY); + drag(e: PointerEvent, delta: Coordinate) { + const block = this.draggingBlock_; + this.moveBlock(block, delta); + this.updateDragTargets(e, block); + this.wouldDeleteBlock_ = this.wouldDeleteBlock(e, block, delta); + this.updateCursorDuringBlockDrag_(); + this.updateConnectionPreview(block, delta); + } + + private moveBlock(draggingBlock: BlockSvg, dragDelta: Coordinate) { + const delta = this.pixelsToWorkspaceUnits_(dragDelta); const newLoc = Coordinate.sum(this.startXY_, delta); - this.draggingBlock_.moveDuringDrag(newLoc); + draggingBlock.moveDuringDrag(newLoc); + } - const oldDragTarget = this.dragTarget_; - this.dragTarget_ = this.workspace_.getDragTarget(e); + private updateDragTargets(e: PointerEvent, draggingBlock: BlockSvg) { + const newDragTarget = this.workspace_.getDragTarget(e); + if (this.dragTarget_ !== newDragTarget) { + this.dragTarget_?.onDragExit(draggingBlock); + newDragTarget?.onDragEnter(draggingBlock); + } + newDragTarget?.onDragOver(draggingBlock); + this.dragTarget_ = newDragTarget; + } - this.draggedConnectionManager_.update(delta, this.dragTarget_); - const oldWouldDeleteBlock = this.wouldDeleteBlock_; - this.wouldDeleteBlock_ = this.draggedConnectionManager_.wouldDeleteBlock; - if (oldWouldDeleteBlock !== this.wouldDeleteBlock_) { - // Prevent unnecessary add/remove class calls. - this.updateCursorDuringBlockDrag_(); + /** + * Returns true if we would delete the block if it was dropped at this time, + * false otherwise. + */ + private wouldDeleteBlock( + e: PointerEvent, + draggingBlock: BlockSvg, + delta: Coordinate, + ): boolean { + const dragTarget = this.workspace_.getDragTarget(e); + if (!dragTarget) return false; + + const componentManager = this.workspace_.getComponentManager(); + const isDeleteArea = componentManager.hasCapability( + dragTarget.id, + ComponentManager.Capability.DELETE_AREA, + ); + if (!isDeleteArea) return false; + + return (dragTarget as IDeleteArea).wouldDelete( + draggingBlock, + !!this.getConnectionCandidate(draggingBlock, delta), + ); + } + + private updateConnectionPreview(draggingBlock: BlockSvg, delta: Coordinate) { + const currCandidate = this.connectionCandidate; + const newCandidate = this.getConnectionCandidate(draggingBlock, delta); + if (!newCandidate) { + this.connectionPreviewer.hidePreview(); + this.connectionCandidate = null; + return; + } + const candidate = + currCandidate && + this.currCandidateIsBetter(currCandidate, delta, newCandidate) + ? currCandidate + : newCandidate; + this.connectionCandidate = candidate; + const {local, neighbour} = candidate; + if ( + (local.type === ConnectionType.OUTPUT_VALUE || + local.type === ConnectionType.PREVIOUS_STATEMENT) && + neighbour.isConnected() && + !neighbour.targetBlock()!.isInsertionMarker() && + !this.orphanCanConnectAtEnd( + draggingBlock, + neighbour.targetBlock()!, + local.type, + ) + ) { + this.connectionPreviewer.previewReplacement( + local, + neighbour, + neighbour.targetBlock()!, + ); + return; + } + this.connectionPreviewer.previewConnection(local, neighbour); + } + + /** + * Returns true if the given orphan block can connect at the end of the + * top block's stack or row, false otherwise. + */ + private orphanCanConnectAtEnd( + topBlock: BlockSvg, + orphanBlock: BlockSvg, + localType: number, + ): boolean { + const orphanConnection = + localType === ConnectionType.OUTPUT_VALUE + ? orphanBlock.outputConnection + : orphanBlock.previousConnection; + return !!Connection.getConnectionForOrphanedConnection( + topBlock as Block, + orphanConnection as Connection, + ); + } + + /** + * Returns true if the current candidate is better than the new candidate. + * + * We slightly prefer the current candidate even if it is farther away. + */ + private currCandidateIsBetter( + currCandiate: ConnectionCandidate, + delta: Coordinate, + newCandidate: ConnectionCandidate, + ): boolean { + const {local: currLocal, neighbour: currNeighbour} = currCandiate; + const localPos = new Coordinate(currLocal.x, currLocal.y); + const neighbourPos = new Coordinate(currNeighbour.x, currNeighbour.y); + const distance = Coordinate.distance( + Coordinate.sum(localPos, delta), + neighbourPos, + ); + return ( + newCandidate.distance > distance - config.currentConnectionPreference + ); + } + + /** + * Returns the closest valid candidate connection, if one can be found. + * + * Valid neighbour connections are within the configured start radius, with a + * compatible type (input, output, etc) and connection check. + */ + private getConnectionCandidate( + draggingBlock: BlockSvg, + delta: Coordinate, + ): ConnectionCandidate | null { + const localConns = this.getLocalConnections(draggingBlock); + let radius = config.snapRadius; + let candidate = null; + + for (const conn of localConns) { + const {connection: neighbour, radius: rad} = conn.closest(radius, delta); + if (neighbour) { + candidate = { + local: conn, + neighbour: neighbour, + distance: rad, + }; + radius = rad; + } } - // Call drag enter/exit/over after wouldDeleteBlock is called in - // InsertionMarkerManager.update. - if (this.dragTarget_ !== oldDragTarget) { - oldDragTarget && oldDragTarget.onDragExit(this.draggingBlock_); - this.dragTarget_ && this.dragTarget_.onDragEnter(this.draggingBlock_); + return candidate; + } + + /** + * Returns all of the connections we might connect to blocks on the workspace. + * + * Includes any connections on the dragging block, and any last next + * connection on the stack (if one exists). + */ + private getLocalConnections(draggingBlock: BlockSvg): RenderedConnection[] { + const available = draggingBlock.getConnections_(false); + const lastOnStack = draggingBlock.lastConnectionInStack(true); + if (lastOnStack && lastOnStack !== draggingBlock.nextConnection) { + available.push(lastOnStack); } - this.dragTarget_ && this.dragTarget_.onDragOver(this.draggingBlock_); + return available; } /** @@ -205,6 +370,7 @@ export class BlockDragger implements IBlockDragger { dom.stopTextWidthCache(); blockAnimation.disconnectUiStop(); + this.connectionPreviewer.hidePreview(); const preventMove = !!this.dragTarget_ && @@ -287,15 +453,33 @@ export class BlockDragger implements IBlockDragger { */ protected updateBlockAfterMove_() { this.fireMoveEvent_(); - if (this.draggedConnectionManager_.wouldConnectBlock()) { + if (this.connectionCandidate) { // Applying connections also rerenders the relevant blocks. - this.draggedConnectionManager_.applyConnections(); + this.applyConnections(this.connectionCandidate); } else { this.draggingBlock_.queueRender(); } this.draggingBlock_.snapToGrid(); } + private applyConnections(candidate: ConnectionCandidate) { + const {local, neighbour} = candidate; + local.connect(neighbour); + // TODO: We can remove this `rendered` check when we reconcile with v11. + if (this.draggingBlock_.rendered) { + const inferiorConnection = local.isSuperior() ? neighbour : local; + const rootBlock = this.draggingBlock_.getRootBlock(); + + finishQueuedRenders().then(() => { + blockAnimation.connectionUiEffect(inferiorConnection.getSourceBlock()); + // bringToFront is incredibly expensive. Delay until the next frame. + setTimeout(() => { + rootBlock.bringToFront(); + }, 0); + }); + } + } + /** Fire a UI event at the end of a block drag. */ protected fireDragEndEvent_() { const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))( @@ -394,14 +578,9 @@ export class BlockDragger implements IBlockDragger { * @returns A possibly empty list of insertion marker blocks. */ getInsertionMarkers(): BlockSvg[] { - // No insertion markers with the old style of dragged connection managers. - if ( - this.draggedConnectionManager_ && - this.draggedConnectionManager_.getInsertionMarkers - ) { - return this.draggedConnectionManager_.getInsertionMarkers(); - } - return []; + return this.workspace_ + .getAllBlocks() + .filter((block) => block.isInsertionMarker()); } } diff --git a/core/block_svg.ts b/core/block_svg.ts index d9f61dd6d..87a8cc6c5 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -1603,7 +1603,8 @@ export class BlockSvg * @internal */ fadeForReplacement(add: boolean) { - this.pathObject.updateReplacementFade(add); + // TODO (7204): Remove these internal methods. + (this.pathObject as AnyDuringMigration).updateReplacementFade(add); } /** @@ -1615,6 +1616,10 @@ export class BlockSvg * @internal */ highlightShapeForInput(conn: RenderedConnection, add: boolean) { - this.pathObject.updateShapeForInputHighlight(conn, add); + // TODO (7204): Remove these internal methods. + (this.pathObject as AnyDuringMigration).updateShapeForInputHighlight( + conn, + add, + ); } } diff --git a/core/blockly.ts b/core/blockly.ts index 96169be27..c7338c37f 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -125,6 +125,7 @@ import {inject} from './inject.js'; import {Input} from './inputs/input.js'; import * as inputs from './inputs.js'; import {InsertionMarkerManager} from './insertion_marker_manager.js'; +import {InsertionMarkerPreviewer} from './connection_previewers/insertion_marker_previewer.js'; import {IASTNodeLocation} from './interfaces/i_ast_node_location.js'; import {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js'; import {IASTNodeLocationWithBlock} from './interfaces/i_ast_node_location_with_block.js'; @@ -135,6 +136,7 @@ import {IBubble} from './interfaces/i_bubble.js'; import {ICollapsibleToolboxItem} from './interfaces/i_collapsible_toolbox_item.js'; import {IComponent} from './interfaces/i_component.js'; import {IConnectionChecker} from './interfaces/i_connection_checker.js'; +import {IConnectionPreviewer} from './interfaces/i_connection_previewer.js'; import {IContextMenu} from './interfaces/i_contextmenu.js'; import {ICopyable, isCopyable} from './interfaces/i_copyable.js'; import {IDeletable} from './interfaces/i_deletable.js'; @@ -555,6 +557,7 @@ export {IBubble}; export {ICollapsibleToolboxItem}; export {IComponent}; export {IConnectionChecker}; +export {IConnectionPreviewer}; export {IContextMenu}; export {icons}; export {ICopyable, isCopyable}; @@ -571,6 +574,7 @@ export {IMovable}; export {Input}; export {inputs}; export {InsertionMarkerManager}; +export {InsertionMarkerPreviewer}; export {IObservable, isObservable}; export {IPaster, isPaster}; export {IPositionable}; diff --git a/core/config.ts b/core/config.ts index ed253e9dd..a6642c266 100644 --- a/core/config.ts +++ b/core/config.ts @@ -48,6 +48,7 @@ export const config: Config = { * Maximum misalignment between connections for them to snap together. * This should be the same as the snap radius. * + * @deprecated v11 - This is no longer used. Use snapRadius instead. */ connectingSnapRadius: DEFAULT_SNAP_RADIUS, /** diff --git a/core/connection_previewers/insertion_marker_previewer.ts b/core/connection_previewers/insertion_marker_previewer.ts new file mode 100644 index 000000000..ef77b6462 --- /dev/null +++ b/core/connection_previewers/insertion_marker_previewer.ts @@ -0,0 +1,235 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {BlockSvg} from '../block_svg.js'; +import {IConnectionPreviewer} from '../interfaces/i_connection_previewer.js'; +import {RenderedConnection} from '../rendered_connection.js'; +import {WorkspaceSvg} from '../workspace_svg.js'; +import * as eventUtils from '../events/utils.js'; +import * as renderManagement from '../render_management.js'; +import * as registry from '../registry.js'; +import * as blocks from '../serialization/blocks.js'; + +/** + * An error message to throw if the block created by createMarkerBlock_ is + * missing any components. + */ +const DUPLICATE_BLOCK_ERROR = + 'The insertion marker previewer tried to create a marker but the result ' + + 'is missing a connection. If you are using a mutator, make sure your ' + + 'domToMutation method is properly defined.'; + +export class InsertionMarkerPreviewer implements IConnectionPreviewer { + private readonly workspace: WorkspaceSvg; + + private fadedBlock: BlockSvg | null = null; + + private markerConn: RenderedConnection | null = null; + + private draggedConn: RenderedConnection | null = null; + + private staticConn: RenderedConnection | null = null; + + constructor(draggedBlock: BlockSvg) { + this.workspace = draggedBlock.workspace; + } + + /** + * Display a connection preview where the draggedCon connects to the + * staticCon, replacing the replacedBlock (currently connected to the + * staticCon). + * + * @param draggedConn The connection on the block stack being dragged. + * @param staticConn The connection not being dragged that we are + * connecting to. + * @param replacedBlock The block currently connected to the staticCon that + * is being replaced. + */ + previewReplacement( + draggedConn: RenderedConnection, + staticConn: RenderedConnection, + replacedBlock: BlockSvg, + ) { + eventUtils.disable(); + try { + this.hidePreview(); + this.fadedBlock = replacedBlock; + replacedBlock.fadeForReplacement(true); + if (this.workspace.getRenderer().shouldHighlightConnection(staticConn)) { + staticConn.highlight(); + this.staticConn = staticConn; + } + } finally { + eventUtils.enable(); + } + } + + /** + * Display a connection preview where the draggedCon connects to the + * staticCon, and no block is being relaced. + * + * @param draggedConn The connection on the block stack being dragged. + * @param staticConn The connection not being dragged that we are + * connecting to. + */ + previewConnection( + draggedConn: RenderedConnection, + staticConn: RenderedConnection, + ) { + if (draggedConn === this.draggedConn && staticConn === this.staticConn) { + return; + } + + eventUtils.disable(); + try { + this.hidePreview(); + const dragged = draggedConn.getSourceBlock(); + const marker = this.createInsertionMarker(dragged); + const markerConn = this.getMatchingConnection( + dragged, + marker, + draggedConn, + ); + if (!markerConn) { + 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'); + }); + + if (this.workspace.getRenderer().shouldHighlightConnection(staticConn)) { + staticConn.highlight(); + } + + this.markerConn = markerConn; + this.draggedConn = draggedConn; + this.staticConn = staticConn; + } finally { + eventUtils.enable(); + } + } + + private createInsertionMarker(origBlock: BlockSvg) { + const blockJson = blocks.save(origBlock, { + addCoordinates: false, + addInputBlocks: false, + addNextBlocks: false, + doFullSerialization: false, + }); + + if (!blockJson) { + throw new Error( + `Failed to serialize source block. ${origBlock.toDevString()}`, + ); + } + + const result = blocks.append(blockJson, this.workspace) as BlockSvg; + + // Turn shadow blocks that are created programmatically during + // initalization to insertion markers too. + for (const block of result.getDescendants(false)) { + block.setInsertionMarker(true); + } + + result.initSvg(); + result.getSvgRoot().setAttribute('visibility', 'hidden'); + return result; + } + + /** + * Gets the connection on the marker block that matches the original + * connection on the original block. + * + * @param orig The original block. + * @param marker The marker block (where we want to find the matching + * connection). + * @param origConn The original connection. + */ + private getMatchingConnection( + orig: BlockSvg, + marker: BlockSvg, + origConn: RenderedConnection, + ) { + const origConns = orig.getConnections_(true); + const markerConns = marker.getConnections_(true); + for (let i = 0; i < origConns.length; i++) { + if (origConns[i] === origConn) { + return markerConns[i]; + } + } + return null; + } + + /** Hide any previews that are currently displayed. */ + hidePreview() { + eventUtils.disable(); + try { + if (this.staticConn) { + this.staticConn.unhighlight(); + this.staticConn = null; + } + if (this.fadedBlock) { + this.fadedBlock.fadeForReplacement(false); + this.fadedBlock = null; + } + if (this.markerConn) { + this.hideInsertionMarker(this.markerConn); + this.markerConn = null; + this.draggedConn = null; + } + } finally { + eventUtils.enable(); + } + } + + private hideInsertionMarker(markerConn: RenderedConnection) { + const marker = markerConn.getSourceBlock(); + const markerPrev = marker.previousConnection; + const markerOutput = marker.outputConnection; + + if (!markerPrev?.targetConnection && !markerOutput?.targetConnection) { + // If we are the top block, unplugging doesn't do anything. + // The marker connection may not have a target block if we are hiding + // as part of applying connections. + markerConn.targetBlock()?.unplug(false); + } else { + marker.unplug(true); + } + + marker.dispose(); + } + + /** Dispose of any references held by this connection previewer. */ + dispose() { + this.hidePreview(); + } +} + +registry.register( + registry.Type.CONNECTION_PREVIEWER, + registry.DEFAULT, + InsertionMarkerPreviewer, +); diff --git a/core/field_input.ts b/core/field_input.ts index 2cd4015b0..513047054 100644 --- a/core/field_input.ts +++ b/core/field_input.ts @@ -542,6 +542,7 @@ export abstract class FieldInput extends Field< } else if (e.key === 'Escape') { this.setValue( this.htmlInput_!.getAttribute('data-untyped-default-value'), + false, ); WidgetDiv.hide(); dropDownDiv.hideWithoutAnimation(); diff --git a/core/flyout_base.ts b/core/flyout_base.ts index 52d9a2298..438e92d64 100644 --- a/core/flyout_base.ts +++ b/core/flyout_base.ts @@ -649,7 +649,7 @@ export abstract class Flyout const parsedContent = toolbox.convertFlyoutDefToJsonArray(flyoutDef); const flyoutInfo = this.createFlyoutInfo(parsedContent); - renderManagement.triggerQueuedRenders(); + renderManagement.triggerQueuedRenders(this.workspace_); this.layout_(flyoutInfo.contents, flyoutInfo.gaps); @@ -1251,8 +1251,7 @@ export abstract class Flyout } // Clone the block. - // TODO(#7432): Add a saveIds parameter to `save`. - const json = blocks.save(oldBlock, {saveIds: false}) as blocks.State; + const json = blocks.save(oldBlock) as blocks.State; // Normallly this resizes leading to weird jumps. Save it for terminateDrag. targetWorkspace.setResizesEnabled(false); const block = blocks.append(json, targetWorkspace) as BlockSvg; diff --git a/core/insertion_marker_manager.ts b/core/insertion_marker_manager.ts index 302dd47c6..ff0f3389d 100644 --- a/core/insertion_marker_manager.ts +++ b/core/insertion_marker_manager.ts @@ -46,6 +46,8 @@ interface CandidateConnection { * Class that controls updates to connections during drags. It is primarily * responsible for finding the closest eligible connection and highlighting or * unhighlighting it as needed during a drag. + * + * @deprecated v10 - Use an IConnectionPreviewer instead. */ export class InsertionMarkerManager { /** diff --git a/core/interfaces/i_connection_previewer.ts b/core/interfaces/i_connection_previewer.ts new file mode 100644 index 000000000..df7906a29 --- /dev/null +++ b/core/interfaces/i_connection_previewer.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {BlockSvg} from '../block_svg'; +import type {RenderedConnection} from '../rendered_connection'; + +/** + * Displays visual "previews" of where a block will be connected if it is + * dropped. + */ +export interface IConnectionPreviewer { + /** + * Display a connection preview where the draggedCon connects to the + * staticCon, replacing the replacedBlock (currently connected to the + * staticCon). + * + * @param draggedCon The connection on the block stack being dragged. + * @param staticCon The connection not being dragged that we are + * connecting to. + * @param replacedBlock The block currently connected to the staticCon that + * is being replaced. + */ + previewReplacement( + draggedConn: RenderedConnection, + staticConn: RenderedConnection, + replacedBlock: BlockSvg, + ): void; + + /** + * Display a connection preview where the draggedCon connects to the + * staticCon, and no block is being relaced. + * + * @param draggedCon The connection on the block stack being dragged. + * @param staticCon The connection not being dragged that we are + * connecting to. + */ + previewConnection( + draggedConn: RenderedConnection, + staticConn: RenderedConnection, + ): void; + + /** Hide any previews that are currently displayed. */ + hidePreview(): void; + + /** Dispose of any references held by this connection previewer. */ + dispose(): void; +} diff --git a/core/registry.ts b/core/registry.ts index 33bded304..3645a6fdf 100644 --- a/core/registry.ts +++ b/core/registry.ts @@ -21,8 +21,9 @@ import type {Options} from './options.js'; import type {Renderer} from './renderers/common/renderer.js'; import type {Theme} from './theme.js'; import type {ToolboxItem} from './toolbox/toolbox_item.js'; -import {IPaster} from './interfaces/i_paster.js'; -import {ICopyData, ICopyable} from './interfaces/i_copyable.js'; +import type {IPaster} from './interfaces/i_paster.js'; +import type {ICopyData, ICopyable} from './interfaces/i_copyable.js'; +import type {IConnectionPreviewer} from './interfaces/i_connection_previewer.js'; /** * A map of maps. With the keys being the type and name of the class we are @@ -66,6 +67,10 @@ export class Type<_T> { static CONNECTION_CHECKER = new Type('connectionChecker'); + static CONNECTION_PREVIEWER = new Type( + 'connectionPreviewer', + ); + static CURSOR = new Type('cursor'); static EVENT = new Type('event'); diff --git a/core/render_management.ts b/core/render_management.ts index c5e3d2af5..6b7e604f4 100644 --- a/core/render_management.ts +++ b/core/render_management.ts @@ -7,12 +7,13 @@ import {BlockSvg} from './block_svg.js'; import * as userAgent from './utils/useragent.js'; import * as eventUtils from './events/utils.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; /** The set of all blocks in need of rendering which don't have parents. */ const rootBlocks = new Set(); /** The set of all blocks in need of rendering. */ -let dirtyBlocks = new WeakSet(); +const dirtyBlocks = new WeakSet(); /** A map from queued blocks to the event group from when they were queued. */ let eventGroups = new WeakMap(); @@ -79,12 +80,14 @@ export function finishQueuedRenders(): Promise { * cases where queueing renders breaks functionality + backwards compatibility * (such as rendering icons). * + * @param workspace If provided, only rerender blocks in this workspace. + * * @internal */ -export function triggerQueuedRenders() { - window.cancelAnimationFrame(animationRequestId); - doRenders(); - if (afterRendersResolver) afterRendersResolver(); +export function triggerQueuedRenders(workspace?: WorkspaceSvg) { + if (!workspace) window.cancelAnimationFrame(animationRequestId); + doRenders(workspace); + if (!workspace && afterRendersResolver) afterRendersResolver(); } /** @@ -115,10 +118,16 @@ function queueBlock(block: BlockSvg) { /** * Rerenders all of the blocks in the queue. + * + * @param workspace If provided, only rerender blocks in this workspace. */ -function doRenders() { - const workspaces = new Set([...rootBlocks].map((block) => block.workspace)); - const blocks = [...rootBlocks].filter(shouldRenderRootBlock); +function doRenders(workspace?: WorkspaceSvg) { + const workspaces = workspace + ? new Set([workspace]) + : new Set([...rootBlocks].map((block) => block.workspace)); + const blocks = [...rootBlocks] + .filter(shouldRenderRootBlock) + .filter((b) => workspaces.has(b.workspace)); for (const block of blocks) { renderBlock(block); } @@ -139,10 +148,20 @@ function doRenders() { eventUtils.setGroup(oldGroup); } - rootBlocks.clear(); - dirtyBlocks = new WeakSet(); - eventGroups = new WeakMap(); - afterRendersPromise = null; + for (const block of blocks) { + dequeueBlock(block); + } + if (!workspace) afterRendersPromise = null; +} + +/** Removes the given block and children from the render queue. */ +function dequeueBlock(block: BlockSvg) { + rootBlocks.delete(block); + dirtyBlocks.delete(block); + eventGroups.delete(block); + for (const child of block.getChildren(false)) { + dequeueBlock(child); + } } /** diff --git a/core/rendered_connection.ts b/core/rendered_connection.ts index 61cc56731..6f0d24188 100644 --- a/core/rendered_connection.ts +++ b/core/rendered_connection.ts @@ -22,19 +22,6 @@ import * as eventUtils from './events/utils.js'; import {hasBubble} from './interfaces/i_has_bubble.js'; import * as internalConstants from './internal_constants.js'; import {Coordinate} from './utils/coordinate.js'; -import * as dom from './utils/dom.js'; -import {Svg} from './utils/svg.js'; -import * as svgPaths from './utils/svg_paths.js'; - -/** A shape that has a pathDown property. */ -interface PathDownShape { - pathDown: string; -} - -/** A shape that has a pathLeft property. */ -interface PathLeftShape { - pathLeft: string; -} /** Maximum randomness in workspace units for bumping a block. */ const BUMP_RANDOMNESS = 10; @@ -49,7 +36,7 @@ export class RenderedConnection extends Connection { private readonly dbOpposite: ConnectionDB; private readonly offsetInBlock: Coordinate; private trackedState: TrackedState; - private highlightPath: SVGPathElement | null = null; + private highlighted: boolean = false; /** Connection this connection connects to. Null if not connected. */ override targetConnection: RenderedConnection | null = null; @@ -92,10 +79,6 @@ export class RenderedConnection extends Connection { if (this.trackedState === RenderedConnection.TrackedState.TRACKED) { this.db.removeConnection(this, this.y); } - if (this.highlightPath) { - dom.removeNode(this.highlightPath); - this.highlightPath = null; - } } /** @@ -305,57 +288,19 @@ export class RenderedConnection extends Connection { /** Add highlighting around this connection. */ highlight() { - if (this.highlightPath) { - // This connection is already highlighted - return; - } - let steps; - const sourceBlockSvg = this.sourceBlock_; - const renderConstants = sourceBlockSvg.workspace - .getRenderer() - .getConstants(); - const shape = renderConstants.shapeFor(this); - if ( - this.type === ConnectionType.INPUT_VALUE || - this.type === ConnectionType.OUTPUT_VALUE - ) { - // Vertical line, puzzle tab, vertical line. - const yLen = renderConstants.TAB_OFFSET_FROM_TOP; - steps = - svgPaths.moveBy(0, -yLen) + - svgPaths.lineOnAxis('v', yLen) + - (shape as unknown as PathDownShape).pathDown + - svgPaths.lineOnAxis('v', yLen); - } else { - const xLen = - renderConstants.NOTCH_OFFSET_LEFT - renderConstants.CORNER_RADIUS; - // Horizontal line, notch, horizontal line. - steps = - svgPaths.moveBy(-xLen, 0) + - svgPaths.lineOnAxis('h', xLen) + - (shape as unknown as PathLeftShape).pathLeft + - svgPaths.lineOnAxis('h', xLen); - } - const offset = this.offsetInBlock; - this.highlightPath = dom.createSvgElement( - Svg.PATH, - { - 'class': 'blocklyHighlightedConnectionPath', - 'd': steps, - 'transform': - `translate(${offset.x}, ${offset.y})` + - (this.sourceBlock_.RTL ? ' scale(-1 1)' : ''), - }, - this.sourceBlock_.getSvgRoot(), - ); + this.highlighted = true; + this.getSourceBlock().queueRender(); } /** Remove the highlighting around this connection. */ unhighlight() { - if (this.highlightPath) { - dom.removeNode(this.highlightPath); - this.highlightPath = null; - } + this.highlighted = false; + this.getSourceBlock().queueRender(); + } + + /** Returns true if this connection is highlighted, false otherwise. */ + isHighlighted(): boolean { + return this.highlighted; } /** diff --git a/core/renderers/common/constants.ts b/core/renderers/common/constants.ts index f0acc39aa..1983793f0 100644 --- a/core/renderers/common/constants.ts +++ b/core/renderers/common/constants.ts @@ -53,8 +53,8 @@ export interface PuzzleTab { type: number; width: number; height: number; - pathDown: string | ((p1: number) => string); - pathUp: string | ((p1: number) => string); + pathDown: string; + pathUp: string; } /** @@ -100,6 +100,22 @@ export function isDynamicShape(shape: Shape): shape is DynamicShape { return (shape as DynamicShape).isDynamic; } +/** Returns whether the shape is a puzzle tab or not. */ +export function isPuzzleTab(shape: Shape): shape is PuzzleTab { + return ( + (shape as PuzzleTab).pathDown !== undefined && + (shape as PuzzleTab).pathUp !== undefined + ); +} + +/** Returns whether the shape is a notch or not. */ +export function isNotch(shape: Shape): shape is Notch { + return ( + (shape as Notch).pathLeft !== undefined && + (shape as Notch).pathRight !== undefined + ); +} + /** * An object that provides constants for rendering blocks. */ diff --git a/core/renderers/common/drawer.ts b/core/renderers/common/drawer.ts index e307b88bc..5ded620b7 100644 --- a/core/renderers/common/drawer.ts +++ b/core/renderers/common/drawer.ts @@ -18,9 +18,10 @@ import type {PreviousConnection} from '../measurables/previous_connection.js'; import type {Row} from '../measurables/row.js'; import {Types} from '../measurables/types.js'; -import {isDynamicShape} from './constants.js'; +import {isDynamicShape, isNotch, isPuzzleTab} from './constants.js'; import type {ConstantProvider, Notch, PuzzleTab} from './constants.js'; import type {RenderInfo} from './info.js'; +import {ConnectionType} from '../../connection_type.js'; /** * An object that draws a block based on the given rendering information. @@ -59,6 +60,7 @@ export class Drawer { draw() { this.drawOutline_(); this.drawInternals_(); + this.updateConnectionHighlights(); this.block_.pathObject.setPath(this.outlinePath_ + '\n' + this.inlinePath_); if (this.info_.RTL) { @@ -429,4 +431,87 @@ export class Drawer { ); } } + + /** + * Updates the path object to reflect which connections on the block are + * highlighted. + */ + protected updateConnectionHighlights() { + for (const row of this.info_.rows) { + for (const elem of row.elements) { + if (!(elem instanceof Connection)) continue; + + if (elem.highlighted) { + this.drawConnectionHighlightPath(elem); + } else { + this.block_.pathObject.removeConnectionHighlight?.( + elem.connectionModel, + ); + } + } + } + } + + /** Returns a path to highlight the given connection. */ + drawConnectionHighlightPath(measurable: Connection) { + const conn = measurable.connectionModel; + let path = ''; + if ( + conn.type === ConnectionType.INPUT_VALUE || + conn.type === ConnectionType.OUTPUT_VALUE + ) { + path = this.getExpressionConnectionHighlightPath(measurable); + } else { + path = this.getStatementConnectionHighlightPath(measurable); + } + const block = conn.getSourceBlock(); + block.pathObject.addConnectionHighlight?.( + conn, + path, + conn.getOffsetInBlock(), + block.RTL, + ); + } + + /** + * Returns a path to highlight the given conneciton, assuming it is an + * input or output connection. + */ + private getExpressionConnectionHighlightPath(connection: Connection): string { + let connPath = ''; + if (isDynamicShape(connection.shape)) { + connPath = connection.shape.pathDown(connection.height); + } else if (isPuzzleTab(connection.shape)) { + connPath = connection.shape.pathDown; + } + + // We are assuming that there is room for the tab offset above and below + // the tab. + const yLen = this.constants_.TAB_OFFSET_FROM_TOP; + return ( + svgPaths.moveBy(0, -yLen) + + svgPaths.lineOnAxis('v', yLen) + + connPath + + svgPaths.lineOnAxis('v', yLen) + ); + } + + /** + * Returns a path to highlight the given conneciton, assuming it is a + * next or previous connection. + */ + private getStatementConnectionHighlightPath(connection: Connection): string { + if (!isNotch(connection.shape)) { + throw new Error('Statement connections should have notch shapes'); + } + + const xLen = + this.constants_.NOTCH_OFFSET_LEFT - this.constants_.CORNER_RADIUS; + return ( + svgPaths.moveBy(-xLen, 0) + + svgPaths.lineOnAxis('h', xLen) + + connection.shape.pathLeft + + svgPaths.lineOnAxis('h', xLen) + ); + } } diff --git a/core/renderers/common/i_path_object.ts b/core/renderers/common/i_path_object.ts index 5f7189bab..30033f18e 100644 --- a/core/renderers/common/i_path_object.ts +++ b/core/renderers/common/i_path_object.ts @@ -9,7 +9,8 @@ import type {BlockStyle} from '../../theme.js'; import type {BlockSvg} from '../../block_svg.js'; import type {ConstantProvider} from './constants.js'; -import {RenderedConnection} from '../../rendered_connection.js'; +import type {RenderedConnection} from '../../rendered_connection.js'; +import type {Coordinate} from '../../utils/coordinate.js'; /** * An interface for a block's path object. @@ -121,21 +122,16 @@ export interface IPathObject { */ updateMovable(enabled: boolean): void; - /** - * Add or remove styling that shows that if the dragging block is dropped, - * this block will be replaced. If a shadow block, it will disappear. - * Otherwise it will bump. - * - * @param enable True if styling should be added. - */ - updateReplacementFade(enabled: boolean): void; + /** Adds the given path as a connection highlight for the given connection. */ + addConnectionHighlight?( + connection: RenderedConnection, + connectionPath: string, + offset: Coordinate, + rtl: boolean, + ): void; /** - * Add or remove styling that shows that if the dragging block is dropped, - * this block will be connected to the input. - * - * @param conn The connection on the input to highlight. - * @param enable True if styling should be added. + * Removes any highlight associated with the given connection, if it exists. */ - updateShapeForInputHighlight(conn: RenderedConnection, enable: boolean): void; + removeConnectionHighlight?(connection: RenderedConnection): void; } diff --git a/core/renderers/common/info.ts b/core/renderers/common/info.ts index fa44b7743..995124c1b 100644 --- a/core/renderers/common/info.ts +++ b/core/renderers/common/info.ts @@ -37,6 +37,7 @@ import {ValueInput} from '../../inputs/value_input.js'; import type {ConstantProvider} from './constants.js'; import type {Renderer} from './renderer.js'; +import {Connection} from '../measurables/connection.js'; /** * An object containing all sizing information needed to draw this block. @@ -143,8 +144,7 @@ export class RenderInfo { } /** - * Populate and return an object containing all sizing information needed to - * draw this block. + * Populate this object with all sizing information needed to draw the block. * * This measure pass does not propagate changes to the block (although fields * may choose to rerender when getSize() is called). However, calling it @@ -748,4 +748,21 @@ export class RenderInfo { this.startY = this.topRow.capline; this.bottomRow.baseline = yCursor - this.bottomRow.descenderHeight; } + + /** Returns the connection measurable associated with the given connection. */ + getMeasureableForConnection(conn: RenderedConnection): Connection | null { + if (this.outputConnection?.connectionModel === conn) { + return this.outputConnection; + } + + for (const row of this.rows) { + for (const elem of row.elements) { + if (elem instanceof Connection && elem.connectionModel === conn) { + return elem; + } + } + } + + return null; + } } diff --git a/core/renderers/common/path_object.ts b/core/renderers/common/path_object.ts index f5987f86e..d5c0850a1 100644 --- a/core/renderers/common/path_object.ts +++ b/core/renderers/common/path_object.ts @@ -8,7 +8,9 @@ import type {BlockSvg} from '../../block_svg.js'; import type {Connection} from '../../connection.js'; +import {RenderedConnection} from '../../rendered_connection.js'; import type {BlockStyle} from '../../theme.js'; +import {Coordinate} from '../../utils/coordinate.js'; import * as dom from '../../utils/dom.js'; import {Svg} from '../../utils/svg.js'; @@ -38,6 +40,12 @@ export class PathObject implements IPathObject { constants: ConstantProvider; style: BlockStyle; + /** Highlight paths associated with connections. */ + private connectionHighlights = new WeakMap(); + + /** Locations of connection highlights. */ + private highlightOffsets = new WeakMap(); + /** * @param root The root SVG element. * @param style The style object to use for colouring. @@ -256,4 +264,53 @@ export class PathObject implements IPathObject { updateShapeForInputHighlight(_conn: Connection, _enable: boolean) { // NOOP } + + /** Adds the given path as a connection highlight for the given connection. */ + addConnectionHighlight( + connection: RenderedConnection, + connectionPath: string, + offset: Coordinate, + rtl: boolean, + ) { + if (this.connectionHighlights.has(connection)) { + if (this.currentHighlightMatchesNew(connection, connectionPath, offset)) { + return; + } + this.removeConnectionHighlight(connection); + } + + const highlight = dom.createSvgElement( + Svg.PATH, + { + 'class': 'blocklyHighlightedConnectionPath', + 'd': connectionPath, + 'transform': + `translate(${offset.x}, ${offset.y})` + (rtl ? ' scale(-1 1)' : ''), + }, + this.svgRoot, + ); + this.connectionHighlights.set(connection, highlight); + } + + private currentHighlightMatchesNew( + connection: RenderedConnection, + newPath: string, + newOffset: Coordinate, + ): boolean { + const currPath = this.connectionHighlights + .get(connection) + ?.getAttribute('d'); + const currOffset = this.highlightOffsets.get(connection); + return currPath === newPath && Coordinate.equals(currOffset, newOffset); + } + + /** + * Removes any highlight associated with the given connection, if it exists. + */ + removeConnectionHighlight(connection: RenderedConnection) { + const highlight = this.connectionHighlights.get(connection); + if (!highlight) return; + dom.removeNode(highlight); + this.connectionHighlights.delete(connection); + } } diff --git a/core/renderers/common/renderer.ts b/core/renderers/common/renderer.ts index 3a53322dd..15a958db4 100644 --- a/core/renderers/common/renderer.ts +++ b/core/renderers/common/renderer.ts @@ -26,6 +26,7 @@ import type {IPathObject} from './i_path_object.js'; import {RenderInfo} from './info.js'; import {MarkerSvg} from './marker_svg.js'; import {PathObject} from './path_object.js'; +import * as deprecation from '../../utils/deprecation.js'; /** * The base class for a block renderer. @@ -231,12 +232,21 @@ export class Renderer implements IRegistrable { * @param local The connection currently being dragged. * @param topBlock The block currently being dragged. * @returns The preview type to display. + * + * @deprecated v10 - This function is no longer respected. A custom + * IConnectionPreviewer may be able to fulfill the functionality. */ getConnectionPreviewMethod( closest: RenderedConnection, local: RenderedConnection, topBlock: BlockSvg, ): PreviewType { + deprecation.warn( + 'getConnectionPreviewMethod', + 'v10', + 'v12', + 'an IConnectionPreviewer, if it fulfills your use case.', + ); if ( local.type === ConnectionType.OUTPUT_VALUE || local.type === ConnectionType.PREVIOUS_STATEMENT diff --git a/core/renderers/geras/drawer.ts b/core/renderers/geras/drawer.ts index fd9a0795e..29bcbfab4 100644 --- a/core/renderers/geras/drawer.ts +++ b/core/renderers/geras/drawer.ts @@ -40,6 +40,7 @@ export class Drawer extends BaseDrawer { override draw() { this.drawOutline_(); this.drawInternals_(); + this.updateConnectionHighlights(); const pathObject = this.block_.pathObject as PathObject; pathObject.setPath(this.outlinePath_ + '\n' + this.inlinePath_); diff --git a/core/renderers/measurables/connection.ts b/core/renderers/measurables/connection.ts index e25215770..5744eaab4 100644 --- a/core/renderers/measurables/connection.ts +++ b/core/renderers/measurables/connection.ts @@ -20,6 +20,7 @@ import {Types} from './types.js'; export class Connection extends Measurable { shape: Shape; isDynamicShape: boolean; + highlighted: boolean; /** * @param constants The rendering constants provider. @@ -32,9 +33,11 @@ export class Connection extends Measurable { ) { super(constants); - this.shape = this.constants_.shapeFor(connectionModel); - - this.isDynamicShape = 'isDynamic' in this.shape && this.shape.isDynamic; this.type |= Types.CONNECTION; + + this.shape = this.constants_.shapeFor(connectionModel); + this.isDynamicShape = 'isDynamic' in this.shape && this.shape.isDynamic; + + this.highlighted = connectionModel.isHighlighted(); } } diff --git a/core/renderers/zelos/constants.ts b/core/renderers/zelos/constants.ts index 73a600c58..1eabd7edd 100644 --- a/core/renderers/zelos/constants.ts +++ b/core/renderers/zelos/constants.ts @@ -392,19 +392,20 @@ export class ConstantProvider extends BaseConstantProvider { blockHeight > maxHeight ? blockHeight - maxHeight : 0; const height = blockHeight > maxHeight ? maxHeight : blockHeight; const radius = height / 2; + const sweep = right === up ? '0' : '1'; return ( svgPaths.arc( 'a', - '0 0,1', + '0 0,' + sweep, radius, - svgPaths.point((up ? -1 : 1) * radius, (up ? -1 : 1) * radius), + svgPaths.point((right ? 1 : -1) * radius, (up ? -1 : 1) * radius), ) + - svgPaths.lineOnAxis('v', (right ? 1 : -1) * remainingHeight) + + svgPaths.lineOnAxis('v', (up ? -1 : 1) * remainingHeight) + svgPaths.arc( 'a', - '0 0,1', + '0 0,' + sweep, radius, - svgPaths.point((up ? 1 : -1) * radius, (up ? -1 : 1) * radius), + svgPaths.point((right ? -1 : 1) * radius, (up ? -1 : 1) * radius), ) ); } @@ -465,19 +466,20 @@ export class ConstantProvider extends BaseConstantProvider { */ function makeMainPath(height: number, up: boolean, right: boolean): string { const innerHeight = height - radius * 2; + const sweep = right === up ? '0' : '1'; return ( svgPaths.arc( 'a', - '0 0,1', + '0 0,' + sweep, radius, - svgPaths.point((up ? -1 : 1) * radius, (up ? -1 : 1) * radius), + svgPaths.point((right ? 1 : -1) * radius, (up ? -1 : 1) * radius), ) + - svgPaths.lineOnAxis('v', (right ? 1 : -1) * innerHeight) + + svgPaths.lineOnAxis('v', (up ? -1 : 1) * innerHeight) + svgPaths.arc( 'a', - '0 0,1', + '0 0,' + sweep, radius, - svgPaths.point((up ? 1 : -1) * radius, (up ? -1 : 1) * radius), + svgPaths.point((right ? -1 : 1) * radius, (up ? -1 : 1) * radius), ) ); } diff --git a/core/renderers/zelos/drawer.ts b/core/renderers/zelos/drawer.ts index ffdc3ca58..009247aea 100644 --- a/core/renderers/zelos/drawer.ts +++ b/core/renderers/zelos/drawer.ts @@ -7,10 +7,13 @@ // Former goog.module ID: Blockly.zelos.Drawer import type {BlockSvg} from '../../block_svg.js'; +import {ConnectionType} from '../../connection_type.js'; import * as svgPaths from '../../utils/svg_paths.js'; import type {BaseShape, DynamicShape, Notch} from '../common/constants.js'; import {Drawer as BaseDrawer} from '../common/drawer.js'; +import {Connection} from '../measurables/connection.js'; import type {InlineInput} from '../measurables/inline_input.js'; +import {OutputConnection} from '../measurables/output_connection.js'; import type {Row} from '../measurables/row.js'; import type {SpacerRow} from '../measurables/spacer_row.js'; import {Types} from '../measurables/types.js'; @@ -41,6 +44,7 @@ export class Drawer extends BaseDrawer { pathObject.beginDrawing(); this.drawOutline_(); this.drawInternals_(); + this.updateConnectionHighlights(); pathObject.setPath(this.outlinePath_ + '\n' + this.inlinePath_); if (this.info_.RTL) { @@ -179,21 +183,27 @@ export class Drawer extends BaseDrawer { return; } - const width = input.width - input.connectionWidth * 2; - const height = input.height; - const yPos = input.centerline - height / 2; - + const yPos = input.centerline - input.height / 2; const connectionRight = input.xPos + input.connectionWidth; - const outlinePath = - svgPaths.moveTo(connectionRight, yPos) + - svgPaths.lineOnAxis('h', width) + - (input.shape as DynamicShape).pathRightDown(input.height) + - svgPaths.lineOnAxis('h', -width) + - (input.shape as DynamicShape).pathUp(input.height) + - 'z'; + const path = + svgPaths.moveTo(connectionRight, yPos) + this.getInlineInputPath(input); + const pathObject = this.block_.pathObject as PathObject; - pathObject.setOutlinePath(inputName, outlinePath); + pathObject.setOutlinePath(inputName, path); + } + + private getInlineInputPath(input: InlineInput) { + const width = input.width - input.connectionWidth * 2; + const height = input.height; + + return ( + svgPaths.lineOnAxis('h', width) + + (input.shape as DynamicShape).pathRightDown(height) + + svgPaths.lineOnAxis('h', -width) + + (input.shape as DynamicShape).pathUp(height) + + 'z' + ); } override drawStatementInput_(row: Row) { @@ -225,4 +235,40 @@ export class Drawer extends BaseDrawer { this.positionStatementInputConnection_(row); } + + /** Returns a path to highlight the given connection. */ + drawConnectionHighlightPath(measurable: Connection) { + const conn = measurable.connectionModel; + if ( + conn.type === ConnectionType.NEXT_STATEMENT || + conn.type === ConnectionType.PREVIOUS_STATEMENT || + (conn.type === ConnectionType.OUTPUT_VALUE && !measurable.isDynamicShape) + ) { + super.drawConnectionHighlightPath(measurable); + return; + } + + let path = ''; + if (conn.type === ConnectionType.INPUT_VALUE) { + const input = measurable as InlineInput; + const xPos = input.connectionWidth; + const yPos = -input.height / 2; + path = svgPaths.moveTo(xPos, yPos) + this.getInlineInputPath(input); + } else { + // Dynamic output. + const output = measurable as OutputConnection; + const xPos = output.width; + const yPos = -output.height / 2; + path = + svgPaths.moveTo(xPos, yPos) + + (output.shape as DynamicShape).pathDown(output.height); + } + const block = conn.getSourceBlock(); + block.pathObject.addConnectionHighlight?.( + conn, + path, + conn.getOffsetInBlock(), + block.RTL, + ); + } } diff --git a/core/renderers/zelos/renderer.ts b/core/renderers/zelos/renderer.ts index a3bd5f70e..354a3f35a 100644 --- a/core/renderers/zelos/renderer.ts +++ b/core/renderers/zelos/renderer.ts @@ -7,7 +7,6 @@ // Former goog.module ID: Blockly.zelos.Renderer import type {BlockSvg} from '../../block_svg.js'; -import type {Connection} from '../../connection.js'; import {ConnectionType} from '../../connection_type.js'; import {InsertionMarkerManager} from '../../insertion_marker_manager.js'; import type {Marker} from '../../keyboard_nav/marker.js'; @@ -23,6 +22,7 @@ import {Drawer} from './drawer.js'; import {RenderInfo} from './info.js'; import {MarkerSvg} from './marker_svg.js'; import {PathObject} from './path_object.js'; +import * as deprecation from '../../utils/deprecation.js'; /** * The zelos renderer. This renderer emulates Scratch-style and MakeCode-style @@ -109,18 +109,21 @@ export class Renderer extends BaseRenderer { return this.constants_; } - override shouldHighlightConnection(conn: Connection) { - return ( - conn.type !== ConnectionType.INPUT_VALUE && - conn.type !== ConnectionType.OUTPUT_VALUE - ); - } - + /** + * @deprecated v10 - This function is no longer respected. A custom + * IConnectionPreviewer may be able to fulfill the functionality. + */ override getConnectionPreviewMethod( closest: RenderedConnection, local: RenderedConnection, topBlock: BlockSvg, ) { + deprecation.warn( + 'getConnectionPreviewMethod', + 'v10', + 'v12', + 'an IConnectionPreviewer, if it fulfills your use case.', + ); if (local.type === ConnectionType.OUTPUT_VALUE) { if (!closest.isConnected()) { return InsertionMarkerManager.PREVIEW_TYPE.INPUT_OUTLINE; diff --git a/core/utils/deprecation.ts b/core/utils/deprecation.ts index da7977c3e..c793b5f57 100644 --- a/core/utils/deprecation.ts +++ b/core/utils/deprecation.ts @@ -6,6 +6,9 @@ // Former goog.module ID: Blockly.utils.deprecation +// Set of previously-emitted warnings. +const previousWarnings = new Set(); + /** * Warn developers that a function or property is deprecated. * @@ -33,5 +36,12 @@ export function warn( if (opt_use) { msg += '\nUse ' + opt_use + ' instead.'; } + + // Don't log deprecation warnings multiple times. + if (previousWarnings.has(msg)) { + return; + } + + previousWarnings.add(msg); console.warn(msg); } diff --git a/core/workspace_audio.ts b/core/workspace_audio.ts index 9f4dc54fe..c2ab93222 100644 --- a/core/workspace_audio.ts +++ b/core/workspace_audio.ts @@ -31,6 +31,9 @@ export class WorkspaceAudio { /** Time that the last sound was played. */ private lastSound_: Date | null = null; + /** Whether the audio is muted or not. */ + private muted: boolean = false; + /** * @param parentWorkspace The parent of the workspace this audio object * belongs to, or null. @@ -121,6 +124,9 @@ export class WorkspaceAudio { * @param opt_volume Volume of sound (0-1). */ play(name: string, opt_volume?: number) { + if (this.muted) { + return; + } const sound = this.sounds.get(name); if (sound) { // Don't play one sound on top of another. @@ -148,4 +154,18 @@ export class WorkspaceAudio { this.parentWorkspace.getAudioManager().play(name, opt_volume); } } + + /** + * @param muted If true, mute sounds. Otherwise, play them. + */ + setMuted(muted: boolean) { + this.muted = muted; + } + + /** + * @returns Whether the audio is currently muted or not. + */ + getMuted(): boolean { + return this.muted; + } } diff --git a/core/xml.ts b/core/xml.ts index c3b2d3ebc..ee72f526b 100644 --- a/core/xml.ts +++ b/core/xml.ts @@ -995,10 +995,7 @@ function domToBlockHeadless( throw TypeError('Shadow block not allowed non-shadow child.'); } } - // Ensure this block doesn't have any variable inputs. - if (block.getVarModels().length) { - throw TypeError('Shadow blocks cannot have variable references.'); - } + block.setShadow(true); } return block; diff --git a/package-lock.json b/package-lock.json index 0a36ffad4..6363468da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "eslint": "^8.4.1", "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^9.0.0", - "eslint-plugin-jsdoc": "^46.2.6", + "eslint-plugin-jsdoc": "^48.0.2", "glob": "^10.3.4", "google-closure-compiler": "^20230802.0.0", "gulp": "^4.0.2", @@ -45,12 +45,15 @@ "markdown-tables-to-json": "^0.1.7", "mocha": "^10.0.0", "patch-package": "^8.0.0", - "prettier": "3.1.0", + "prettier": "3.1.1", "readline-sync": "^1.4.10", "rimraf": "^5.0.0", "typescript": "^5.0.2", "webdriverio": "^8.16.7", "yargs": "^17.2.1" + }, + "engines": { + "node": ">=18" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -148,9 +151,9 @@ } }, "node_modules/@blockly/theme-modern": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@blockly/theme-modern/-/theme-modern-5.0.4.tgz", - "integrity": "sha512-AamkRgc5XDvENdEBol8GVUebBooAaHP/yGfixVQ3Oj48xErKisUbLuCpZ4emvahewGghJ55HVXSJKdLQ2n0h8w==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@blockly/theme-modern/-/theme-modern-5.0.5.tgz", + "integrity": "sha512-rbVOGxKHAatzHI6Yhy9lJbIRPzAW2Xgf+N1U1KSkyVmUziLKKaNKwwYvnOSx4MmoDD49SrZMdUgT8G+VBLFhYw==", "dev": true, "engines": { "node": ">=8.17.0" @@ -984,16 +987,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.2.tgz", - "integrity": "sha512-3+9OGAWHhk4O1LlcwLBONbdXsAhLjyCFogJY/cWy2lxdVJ2JrcTF2pTGMaLl2AE7U1l31n8Py4a8bx5DLf/0dQ==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.19.0.tgz", + "integrity": "sha512-DUCUkQNklCQYnrBSSikjVChdc84/vMPDQSgJTHBZ64G9bA9w0Crc0rd2diujKbTdp6w2J47qkeHQLoi0rpLCdg==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.13.2", - "@typescript-eslint/type-utils": "6.13.2", - "@typescript-eslint/utils": "6.13.2", - "@typescript-eslint/visitor-keys": "6.13.2", + "@typescript-eslint/scope-manager": "6.19.0", + "@typescript-eslint/type-utils": "6.19.0", + "@typescript-eslint/utils": "6.19.0", + "@typescript-eslint/visitor-keys": "6.19.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -1019,13 +1022,13 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.2.tgz", - "integrity": "sha512-CXQA0xo7z6x13FeDYCgBkjWzNqzBn8RXaE3QVQVIUm74fWJLkJkaHmHdKStrxQllGh6Q4eUGyNpMe0b1hMkXFA==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.0.tgz", + "integrity": "sha512-dO1XMhV2ehBI6QN8Ufi7I10wmUovmLU0Oru3n5LVlM2JuzB4M+dVphCPLkVpKvGij2j/pHBWuJ9piuXx+BhzxQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.13.2", - "@typescript-eslint/visitor-keys": "6.13.2" + "@typescript-eslint/types": "6.19.0", + "@typescript-eslint/visitor-keys": "6.19.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1036,9 +1039,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.2.tgz", - "integrity": "sha512-7sxbQ+EMRubQc3wTfTsycgYpSujyVbI1xw+3UMRUcrhSy+pN09y/lWzeKDbvhoqcRbHdc+APLs/PWYi/cisLPg==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.0.tgz", + "integrity": "sha512-lFviGV/vYhOy3m8BJ/nAKoAyNhInTdXpftonhWle66XHAtT1ouBlkjL496b5H5hb8dWXHwtypTqgtb/DEa+j5A==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1049,12 +1052,12 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.2.tgz", - "integrity": "sha512-OGznFs0eAQXJsp+xSd6k/O1UbFi/K/L7WjqeRoFE7vadjAF9y0uppXhYNQNEqygjou782maGClOoZwPqF0Drlw==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.0.tgz", + "integrity": "sha512-hZaUCORLgubBvtGpp1JEFEazcuEdfxta9j4iUwdSAr7mEsYYAp3EAUyCZk3VEEqGj6W+AV4uWyrDGtrlawAsgQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.13.2", + "@typescript-eslint/types": "6.19.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -1113,13 +1116,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.2.tgz", - "integrity": "sha512-Qr6ssS1GFongzH2qfnWKkAQmMUyZSyOr0W54nZNU1MDfo+U4Mv3XveeLZzadc/yq8iYhQZHYT+eoXJqnACM1tw==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.19.0.tgz", + "integrity": "sha512-mcvS6WSWbjiSxKCwBcXtOM5pRkPQ6kcDds/juxcy/727IQr3xMEcwr/YLHW2A2+Fp5ql6khjbKBzOyjuPqGi/w==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.13.2", - "@typescript-eslint/utils": "6.13.2", + "@typescript-eslint/typescript-estree": "6.19.0", + "@typescript-eslint/utils": "6.19.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -1140,9 +1143,9 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.2.tgz", - "integrity": "sha512-7sxbQ+EMRubQc3wTfTsycgYpSujyVbI1xw+3UMRUcrhSy+pN09y/lWzeKDbvhoqcRbHdc+APLs/PWYi/cisLPg==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.0.tgz", + "integrity": "sha512-lFviGV/vYhOy3m8BJ/nAKoAyNhInTdXpftonhWle66XHAtT1ouBlkjL496b5H5hb8dWXHwtypTqgtb/DEa+j5A==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1153,16 +1156,17 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.2.tgz", - "integrity": "sha512-SuD8YLQv6WHnOEtKv8D6HZUzOub855cfPnPMKvdM/Bh1plv1f7Q/0iFUDLKKlxHcEstQnaUU4QZskgQq74t+3w==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.0.tgz", + "integrity": "sha512-o/zefXIbbLBZ8YJ51NlkSAt2BamrK6XOmuxSR3hynMIzzyMY33KuJ9vuMdFSXW+H0tVvdF9qBPTHA91HDb4BIQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.13.2", - "@typescript-eslint/visitor-keys": "6.13.2", + "@typescript-eslint/types": "6.19.0", + "@typescript-eslint/visitor-keys": "6.19.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", + "minimatch": "9.0.3", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" }, @@ -1180,12 +1184,12 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.2.tgz", - "integrity": "sha512-OGznFs0eAQXJsp+xSd6k/O1UbFi/K/L7WjqeRoFE7vadjAF9y0uppXhYNQNEqygjou782maGClOoZwPqF0Drlw==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.0.tgz", + "integrity": "sha512-hZaUCORLgubBvtGpp1JEFEazcuEdfxta9j4iUwdSAr7mEsYYAp3EAUyCZk3VEEqGj6W+AV4uWyrDGtrlawAsgQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.13.2", + "@typescript-eslint/types": "6.19.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -1196,6 +1200,30 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/types": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.1.0.tgz", @@ -1239,17 +1267,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.2.tgz", - "integrity": "sha512-b9Ptq4eAZUym4idijCRzl61oPCwwREcfDI8xGk751Vhzig5fFZR9CyzDz4Sp/nxSLBYxUPyh4QdIDqWykFhNmQ==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.19.0.tgz", + "integrity": "sha512-QR41YXySiuN++/dC9UArYOg4X86OAYP83OWTewpVx5ct1IZhjjgTLocj7QNxGhWoTqknsgpl7L+hGygCO+sdYw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.13.2", - "@typescript-eslint/types": "6.13.2", - "@typescript-eslint/typescript-estree": "6.13.2", + "@typescript-eslint/scope-manager": "6.19.0", + "@typescript-eslint/types": "6.19.0", + "@typescript-eslint/typescript-estree": "6.19.0", "semver": "^7.5.4" }, "engines": { @@ -1264,13 +1292,13 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.2.tgz", - "integrity": "sha512-CXQA0xo7z6x13FeDYCgBkjWzNqzBn8RXaE3QVQVIUm74fWJLkJkaHmHdKStrxQllGh6Q4eUGyNpMe0b1hMkXFA==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.0.tgz", + "integrity": "sha512-dO1XMhV2ehBI6QN8Ufi7I10wmUovmLU0Oru3n5LVlM2JuzB4M+dVphCPLkVpKvGij2j/pHBWuJ9piuXx+BhzxQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.13.2", - "@typescript-eslint/visitor-keys": "6.13.2" + "@typescript-eslint/types": "6.19.0", + "@typescript-eslint/visitor-keys": "6.19.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1281,9 +1309,9 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.2.tgz", - "integrity": "sha512-7sxbQ+EMRubQc3wTfTsycgYpSujyVbI1xw+3UMRUcrhSy+pN09y/lWzeKDbvhoqcRbHdc+APLs/PWYi/cisLPg==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.0.tgz", + "integrity": "sha512-lFviGV/vYhOy3m8BJ/nAKoAyNhInTdXpftonhWle66XHAtT1ouBlkjL496b5H5hb8dWXHwtypTqgtb/DEa+j5A==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1294,16 +1322,17 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.2.tgz", - "integrity": "sha512-SuD8YLQv6WHnOEtKv8D6HZUzOub855cfPnPMKvdM/Bh1plv1f7Q/0iFUDLKKlxHcEstQnaUU4QZskgQq74t+3w==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.0.tgz", + "integrity": "sha512-o/zefXIbbLBZ8YJ51NlkSAt2BamrK6XOmuxSR3hynMIzzyMY33KuJ9vuMdFSXW+H0tVvdF9qBPTHA91HDb4BIQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.13.2", - "@typescript-eslint/visitor-keys": "6.13.2", + "@typescript-eslint/types": "6.19.0", + "@typescript-eslint/visitor-keys": "6.19.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", + "minimatch": "9.0.3", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" }, @@ -1321,12 +1350,12 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.2.tgz", - "integrity": "sha512-OGznFs0eAQXJsp+xSd6k/O1UbFi/K/L7WjqeRoFE7vadjAF9y0uppXhYNQNEqygjou782maGClOoZwPqF0Drlw==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.0.tgz", + "integrity": "sha512-hZaUCORLgubBvtGpp1JEFEazcuEdfxta9j4iUwdSAr7mEsYYAp3EAUyCZk3VEEqGj6W+AV4uWyrDGtrlawAsgQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.13.2", + "@typescript-eslint/types": "6.19.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -1337,6 +1366,30 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/utils/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/visitor-keys": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.1.0.tgz", @@ -1362,18 +1415,18 @@ "dev": true }, "node_modules/@wdio/config": { - "version": "8.24.6", - "resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.24.6.tgz", - "integrity": "sha512-ZFmd6rB1kgL4k/SjLXbtFTCxvxSf1qzdt/losiTqkqFBYznkTRUBGSoGaVTlkMtHAReiVSK92sICc15JWaCdEA==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.29.1.tgz", + "integrity": "sha512-zNUac4lM429HDKAitO+fdlwUH1ACQU8lww+DNVgUyuEb86xgVdTqHeiJr/3kOMJAq9IATeE7mDtYyyn6HPm1JA==", "dev": true, "dependencies": { - "@wdio/logger": "8.16.17", - "@wdio/types": "8.24.2", - "@wdio/utils": "8.24.6", + "@wdio/logger": "8.28.0", + "@wdio/types": "8.29.1", + "@wdio/utils": "8.29.1", "decamelize": "^6.0.0", "deepmerge-ts": "^5.0.0", "glob": "^10.2.2", - "import-meta-resolve": "^3.0.0" + "import-meta-resolve": "^4.0.0" }, "engines": { "node": "^16.13 || >=18" @@ -1392,9 +1445,9 @@ } }, "node_modules/@wdio/logger": { - "version": "8.16.17", - "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.16.17.tgz", - "integrity": "sha512-zeQ41z3T+b4IsrriZZipayXxLNDuGsm7TdExaviNGojPVrIsQUCSd/FvlLHM32b7ZrMyInHenu/zx1cjAZO71g==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.28.0.tgz", + "integrity": "sha512-/s6zNCqwy1hoc+K4SJypis0Ud0dlJ+urOelJFO1x0G0rwDRWyFiUP6ijTaCcFxAm29jYEcEPWijl2xkVIHwOyA==", "dev": true, "dependencies": { "chalk": "^5.1.2", @@ -1446,15 +1499,15 @@ } }, "node_modules/@wdio/protocols": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-8.23.0.tgz", - "integrity": "sha512-2XTzD+lqQP3g8BWn+Bn5BTFzjHqzZNwq7DjlYrb27Bq8nOA+1DEcj3WzQ6V6CktTnKI/LAYKA1IFAF//Azrp/Q==", + "version": "8.24.12", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-8.24.12.tgz", + "integrity": "sha512-QnVj3FkapmVD3h2zoZk+ZQ8gevSj9D9MiIQIy8eOnY4FAneYZ9R9GvoW+mgNcCZO8S8++S/jZHetR8n+8Q808g==", "dev": true }, "node_modules/@wdio/repl": { - "version": "8.23.1", - "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-8.23.1.tgz", - "integrity": "sha512-u6zG2cgBm67V5/WlQzadWqLGXs3moH8MOsgoljULQncelSBBZGZ5DyLB4p7jKcUAsKtMjgmFQmIvpQoqmyvdfg==", + "version": "8.24.12", + "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-8.24.12.tgz", + "integrity": "sha512-321F3sWafnlw93uRTSjEBVuvWCxTkWNDs7ektQS15drrroL3TMeFOynu4rDrIz0jXD9Vas0HCD2Tq/P0uxFLdw==", "dev": true, "dependencies": { "@types/node": "^20.1.0" @@ -1464,9 +1517,9 @@ } }, "node_modules/@wdio/types": { - "version": "8.24.2", - "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.24.2.tgz", - "integrity": "sha512-x7iWF5NM8NfVxziGwLdQ+3sstgSxRoqfmmFEDTDps0oFrN5CgkqcoLkqXJ5u166gvpxpEq0gxZwxkbPC/Lp0cw==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.29.1.tgz", + "integrity": "sha512-rZYzu+sK8zY1PjCEWxNu4ELJPYKDZRn7HFcYNgR122ylHygfldwkb5TioI6Pn311hQH/S+663KEeoq//Jb0f8A==", "dev": true, "dependencies": { "@types/node": "^20.1.0" @@ -1476,21 +1529,20 @@ } }, "node_modules/@wdio/utils": { - "version": "8.24.6", - "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.24.6.tgz", - "integrity": "sha512-qwcshLH9iKnhK0jXoXjPw3G02UhyShT0I+ljC0hMybJEBsra92TYFa47Cp6n1fdvM3+/BTuhsgtzRz0anObicQ==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.29.1.tgz", + "integrity": "sha512-Dm91DKL/ZKeZ2QogWT8Twv0p+slEgKyB/5x9/kcCG0Q2nNa+tZedTjOhryzrsPiWc+jTSBmjGE4katRXpJRFJg==", "dev": true, "dependencies": { "@puppeteer/browsers": "^1.6.0", - "@wdio/logger": "8.16.17", - "@wdio/types": "8.24.2", + "@wdio/logger": "8.28.0", + "@wdio/types": "8.29.1", "decamelize": "^6.0.0", "deepmerge-ts": "^5.1.0", "edgedriver": "^5.3.5", "geckodriver": "^4.2.0", "get-port": "^7.0.0", - "got": "^13.0.0", - "import-meta-resolve": "^3.0.0", + "import-meta-resolve": "^4.0.0", "locate-app": "^2.1.0", "safaridriver": "^0.1.0", "split2": "^4.2.0", @@ -1501,9 +1553,9 @@ } }, "node_modules/@wdio/utils/node_modules/@puppeteer/browsers": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.8.0.tgz", - "integrity": "sha512-TkRHIV6k2D8OlUe8RtG+5jgOF/H98Myx0M6AOafC8DdNVOFiBSFa5cpRDtpm8LXOa9sVwe0+e6Q3FC56X/DZfg==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.9.1.tgz", + "integrity": "sha512-PuvK6xZzGhKPvlx3fpfdM2kYY3P/hB1URtK8wA7XUJ6prn6pp22zvJHu48th0SGcHL9SutbPHrFuQgfXTFobWA==", "dev": true, "dependencies": { "debug": "4.3.4", @@ -3513,9 +3565,9 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1213968", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1213968.tgz", - "integrity": "sha512-o4n/beY+3CcZwFctYapjGelKptR4AuQT5gXS1Kvgbig+ArwkxK7f8wDVuD1wsoswiJWCwV6OK+Qb7vhNzNmABQ==", + "version": "0.0.1249869", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1249869.tgz", + "integrity": "sha512-Ctp4hInA0BEavlUoRy9mhGq0i+JSo/AwVyX2EFgZmV1kYB+Zq+EMBAn52QWu6FbRr10hRb6pBl420upbp4++vg==", "dev": true }, "node_modules/dir-glob": { @@ -3661,9 +3713,9 @@ } }, "node_modules/edgedriver": { - "version": "5.3.8", - "resolved": "https://registry.npmjs.org/edgedriver/-/edgedriver-5.3.8.tgz", - "integrity": "sha512-FWLPDuwJDeGGgtmlqTXb4lQi/HV9yylLo1F9O1g9TLqSemA5T6xH28seUIfyleVirLFtDQyKNUxKsMhMT4IfnA==", + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/edgedriver/-/edgedriver-5.3.9.tgz", + "integrity": "sha512-G0wNgFMFRDnFfKaXG2R6HiyVHqhKwdQ3EgoxW3wPlns2wKqem7F+HgkWBcevN7Vz0nN4AXtskID7/6jsYDXcKw==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -3940,9 +3992,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", - "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, "bin": { "eslint-config-prettier": "bin/cli.js" @@ -3952,9 +4004,9 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "46.9.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.9.0.tgz", - "integrity": "sha512-UQuEtbqLNkPf5Nr/6PPRCtr9xypXY+g8y/Q7gPa0YK7eDhh0y2lWprXRnaYbW7ACgIUvpDKy9X2bZqxtGzBG9Q==", + "version": "48.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-48.0.2.tgz", + "integrity": "sha512-CBFl5Jc7+jlV36RwDm+PQ8Uw5r28pn2/uW/OaB+Gw5bFwn4Py/1eYMZ3hGf9S4meUFZ/sRvS+hVif2mRAp6WqQ==", "dev": true, "dependencies": { "@es-joy/jsdoccomment": "~0.41.0", @@ -3965,13 +4017,23 @@ "esquery": "^1.5.0", "is-builtin-module": "^3.2.1", "semver": "^7.5.4", - "spdx-expression-parse": "^3.0.1" + "spdx-expression-parse": "^4.0.0" }, "engines": { - "node": ">=16" + "node": ">=18" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" } }, "node_modules/eslint-scope": { @@ -4666,9 +4728,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", "dev": true, "funding": [ { @@ -4898,17 +4960,17 @@ "dev": true }, "node_modules/geckodriver": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-4.2.1.tgz", - "integrity": "sha512-4m/CRk0OI8MaANRuFIahvOxYTSjlNAO2p9JmE14zxueknq6cdtB5M9UGRQ8R9aMV0bLGNVHHDnDXmoXdOwJfWg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-4.3.1.tgz", + "integrity": "sha512-ol7JLsj55o5k+z7YzeSy2mdJROXMAxIa+uzr3A1yEMr5HISqQOTslE3ZeARcxR4jpAY3fxmHM+sq32qbe/eXfA==", "dev": true, "hasInstallScript": true, "dependencies": { - "@wdio/logger": "^8.11.0", + "@wdio/logger": "^8.24.12", "decamelize": "^6.0.0", "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.1", + "https-proxy-agent": "^7.0.2", + "node-fetch": "^3.3.2", "tar-fs": "^3.0.4", "unzipper": "^0.10.14", "which": "^4.0.0" @@ -5629,9 +5691,9 @@ ] }, "node_modules/got": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz", - "integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==", + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", + "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", "dev": true, "dependencies": { "@sindresorhus/is": "^5.2.0", @@ -5647,7 +5709,7 @@ "responselike": "^3.0.0" }, "engines": { - "node": ">=16" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sindresorhus/got?sponsor=1" @@ -6544,9 +6606,9 @@ } }, "node_modules/import-meta-resolve": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-3.1.1.tgz", - "integrity": "sha512-qeywsE/KC3w9Fd2ORrRDUw6nS/nLwZpXgfrOc2IILvZYnCaEMd+D56Vfg9k4G29gIeVi3XKql1RQatME8iYsiw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.0.0.tgz", + "integrity": "sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA==", "dev": true, "funding": { "type": "github", @@ -7389,12 +7451,12 @@ } }, "node_modules/locate-app": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/locate-app/-/locate-app-2.1.0.tgz", - "integrity": "sha512-rcVo/iLUxrd9d0lrmregK/Z5Y5NCpSwf9KlMbPpOHmKmdxdQY1Fj8NDQ5QymJTryCsBLqwmniFv2f3JKbk9Bvg==", + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/locate-app/-/locate-app-2.2.14.tgz", + "integrity": "sha512-fqGE0IHZ3v+9kCjYvhwrP52aTGP1itOfp4TZZuv4dNl2gKN/pHCIlMhDSqPDb3qJ5Rti39y5T+/XrfCsiDRjKw==", "dev": true, "dependencies": { - "n12": "0.4.0", + "n12": "1.8.17", "type-fest": "2.13.0", "userhome": "1.0.0" } @@ -7498,9 +7560,9 @@ } }, "node_modules/loglevel": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz", - "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.1.tgz", + "integrity": "sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==", "dev": true, "engines": { "node": ">= 0.6.0" @@ -8091,9 +8153,9 @@ } }, "node_modules/n12": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/n12/-/n12-0.4.0.tgz", - "integrity": "sha512-p/hj4zQ8d3pbbFLQuN1K9honUxiDDhueOWyFLw/XgBv+wZCE44bcLH4CIcsolOceJQduh4Jf7m/LfaTxyGmGtQ==", + "version": "1.8.17", + "resolved": "https://registry.npmjs.org/n12/-/n12-1.8.17.tgz", + "integrity": "sha512-/NdfkU7nyqq70E4RvDa3OrR/wkZrYjDGXjn4JgIZnn+ULcyW1f6BLjNqyFC+ND9FqzyWjcQhvagFJmVJ8k8Lew==", "dev": true }, "node_modules/nanoid": { @@ -9044,9 +9106,9 @@ } }, "node_modules/prettier": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", - "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", + "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -9680,9 +9742,9 @@ "dev": true }, "node_modules/safaridriver": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/safaridriver/-/safaridriver-0.1.0.tgz", - "integrity": "sha512-azzzIP3gR1TB9bVPv7QO4Zjw0rR1BWEU/s2aFdUMN48gxDjxEB13grAEuXDmkKPgE74cObymDxmAmZnL3clj4w==", + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/safaridriver/-/safaridriver-0.1.2.tgz", + "integrity": "sha512-4R309+gWflJktzPXBQCobbWEHlzC4aK3a+Ov3tz2Ib2aBxiwd11phkdIBH1l0EO22x24CJMUQkpKFumRriCSRg==", "dev": true }, "node_modules/safe-buffer": { @@ -11379,29 +11441,29 @@ } }, "node_modules/web-streams-polyfill": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.2.tgz", + "integrity": "sha512-3pRGuxRF5gpuZc0W+EpwQRmCD7gRqcDOMt688KmdlDAgAyaB1XlN0zq2njfDNm44XVdIouE7pZ6GzbdyH47uIQ==", "dev": true, "engines": { "node": ">= 8" } }, "node_modules/webdriver": { - "version": "8.24.6", - "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.24.6.tgz", - "integrity": "sha512-k5XI2/SHd/14h4ElPQH8EzSUXujZIGbBEi+3dTS2H457KFR5Q8QYfIazDs/YnEdooOp8b6Oe9N7qI99LP8K6bQ==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.29.1.tgz", + "integrity": "sha512-D3gkbDUxFKBJhNHRfMriWclooLbNavVQC1MRvmENAgPNKaHnFn+M+WtP9K2sEr0XczLGNlbOzT7CKR9K5UXKXA==", "dev": true, "dependencies": { "@types/node": "^20.1.0", "@types/ws": "^8.5.3", - "@wdio/config": "8.24.6", - "@wdio/logger": "8.16.17", - "@wdio/protocols": "8.23.0", - "@wdio/types": "8.24.2", - "@wdio/utils": "8.24.6", + "@wdio/config": "8.29.1", + "@wdio/logger": "8.28.0", + "@wdio/protocols": "8.24.12", + "@wdio/types": "8.29.1", + "@wdio/utils": "8.29.1", "deepmerge-ts": "^5.1.0", - "got": "^ 12.6.1", + "got": "^12.6.1", "ky": "^0.33.0", "ws": "^8.8.0" }, @@ -11409,63 +11471,26 @@ "node": "^16.13 || >=18" } }, - "node_modules/webdriver/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/webdriver/node_modules/got": { - "version": "12.6.1", - "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", - "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", - "dev": true, - "dependencies": { - "@sindresorhus/is": "^5.2.0", - "@szmarczak/http-timer": "^5.0.1", - "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.8", - "decompress-response": "^6.0.0", - "form-data-encoder": "^2.1.2", - "get-stream": "^6.0.1", - "http2-wrapper": "^2.1.10", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^3.0.0", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, "node_modules/webdriverio": { - "version": "8.24.6", - "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.24.6.tgz", - "integrity": "sha512-gJMAJiErbXe/oFJbV+H9lXp9GPxnUgHrbtxkG6SCKQlk1zPFho9FZ3fQWl/ty84w5n9ZMhAdnQIfZM9aytxIBQ==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.29.1.tgz", + "integrity": "sha512-NZK95ivXCqdPraB3FHMw6ByxnCvtgFXkjzG2l3Oq5z0IuJS2aMow3AKFIyiuG6is/deGCe+Tb8eFTCqak7UV+w==", "dev": true, "dependencies": { "@types/node": "^20.1.0", - "@wdio/config": "8.24.6", - "@wdio/logger": "8.16.17", - "@wdio/protocols": "8.23.0", - "@wdio/repl": "8.23.1", - "@wdio/types": "8.24.2", - "@wdio/utils": "8.24.6", + "@wdio/config": "8.29.1", + "@wdio/logger": "8.28.0", + "@wdio/protocols": "8.24.12", + "@wdio/repl": "8.24.12", + "@wdio/types": "8.29.1", + "@wdio/utils": "8.29.1", "archiver": "^6.0.0", "aria-query": "^5.0.0", "css-shorthand-properties": "^1.1.1", "css-value": "^0.0.1", - "devtools-protocol": "^0.0.1213968", + "devtools-protocol": "^0.0.1249869", "grapheme-splitter": "^1.0.2", - "import-meta-resolve": "^3.0.0", + "import-meta-resolve": "^4.0.0", "is-plain-obj": "^4.1.0", "lodash.clonedeep": "^4.5.0", "lodash.zip": "^4.2.0", @@ -11475,7 +11500,7 @@ "resq": "^1.9.1", "rgb2hex": "0.2.5", "serialize-error": "^11.0.1", - "webdriver": "8.24.6" + "webdriver": "8.29.1" }, "engines": { "node": "^16.13 || >=18" diff --git a/package.json b/package.json index d93dc6ca4..36921f2fc 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "eslint": "^8.4.1", "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^9.0.0", - "eslint-plugin-jsdoc": "^46.2.6", + "eslint-plugin-jsdoc": "^48.0.2", "glob": "^10.3.4", "google-closure-compiler": "^20230802.0.0", "gulp": "^4.0.2", @@ -97,7 +97,7 @@ "markdown-tables-to-json": "^0.1.7", "mocha": "^10.0.0", "patch-package": "^8.0.0", - "prettier": "3.1.0", + "prettier": "3.1.1", "readline-sync": "^1.4.10", "rimraf": "^5.0.0", "typescript": "^5.0.2", diff --git a/tests/mocha/render_management_test.js b/tests/mocha/render_management_test.js index 9d368ef7b..7e22cfffb 100644 --- a/tests/mocha/render_management_test.js +++ b/tests/mocha/render_management_test.js @@ -59,4 +59,69 @@ suite('Render Management', function () { return promise; }); }); + + suite('triggering queued renders', function () { + function createMockBlock(ws) { + return { + hasRendered: false, + renderEfficiently: function () { + this.hasRendered = true; + }, + + // All of the APIs the render management system needs. + getParent: () => null, + getChildren: () => [], + isDisposed: () => false, + getRelativeToSurfaceXY: () => ({x: 0, y: 0}), + updateComponentLocations: () => {}, + workspace: ws || createMockWorkspace(), + }; + } + + function createMockWorkspace() { + return { + resizeContents: () => {}, + }; + } + + test('triggering queued renders rerenders blocks', function () { + const block = createMockBlock(); + Blockly.renderManagement.queueRender(block); + + Blockly.renderManagement.triggerQueuedRenders(); + + chai.assert.isTrue(block.hasRendered, 'Expected block to be rendered'); + }); + + test('triggering queued renders rerenders blocks in all workspaces', function () { + const workspace1 = createMockWorkspace(); + const workspace2 = createMockWorkspace(); + const block1 = createMockBlock(workspace1); + const block2 = createMockBlock(workspace2); + Blockly.renderManagement.queueRender(block1); + Blockly.renderManagement.queueRender(block2); + + Blockly.renderManagement.triggerQueuedRenders(); + + chai.assert.isTrue(block1.hasRendered, 'Expected block1 to be rendered'); + chai.assert.isTrue(block2.hasRendered, 'Expected block2 to be rendered'); + }); + + test('triggering queued renders in one workspace does not rerender blocks in another workspace', function () { + const workspace1 = createMockWorkspace(); + const workspace2 = createMockWorkspace(); + const block1 = createMockBlock(workspace1); + const block2 = createMockBlock(workspace2); + Blockly.renderManagement.queueRender(block1); + Blockly.renderManagement.queueRender(block2); + + Blockly.renderManagement.triggerQueuedRenders(workspace1); + + chai.assert.isTrue(block1.hasRendered, 'Expected block1 to be rendered'); + chai.assert.isFalse( + block2.hasRendered, + 'Expected block2 to not be rendered', + ); + }); + }); }); diff --git a/tests/typescript/src/generators.ts b/tests/typescript/src/generators.ts new file mode 100644 index 000000000..a87d70ee3 --- /dev/null +++ b/tests/typescript/src/generators.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as Blockly from 'blockly-test/core'; +import {JavascriptGenerator} from 'blockly-test/javascript'; +import {PhpGenerator, phpGenerator, Order} from 'blockly-test/php'; +import {LuaGenerator} from 'blockly-test/lua'; +import {PythonGenerator} from 'blockly-test/python'; +import {DartGenerator} from 'blockly-test/dart'; + +JavascriptGenerator; +PhpGenerator; +LuaGenerator; +PythonGenerator; +DartGenerator; + +class TestGenerator extends PhpGenerator {} + +const testGenerator = new TestGenerator(); + +testGenerator.forBlock['test_block'] = function ( + _block: Blockly.Block, + _generator: TestGenerator, +) { + return ['a fake code string', Order.ADDITION]; +}; + +phpGenerator.quote_();