diff --git a/core/connection.ts b/core/connection.ts index c7c9a3da691..833a2ddef81 100644 --- a/core/connection.ts +++ b/core/connection.ts @@ -648,7 +648,7 @@ export class Connection implements IASTNodeLocationWithBlock { } if (shadowDom) { - blockShadow = Xml.domToBlock(shadowDom, parentBlock.workspace); + blockShadow = Xml.domToBlockInternal(shadowDom, parentBlock.workspace); if (attemptToConnect) { if (this.type === ConnectionType.INPUT_VALUE) { if (!blockShadow.outputConnection) { diff --git a/core/contextmenu.ts b/core/contextmenu.ts index dc8cf15122f..89f9974c933 100644 --- a/core/contextmenu.ts +++ b/core/contextmenu.ts @@ -235,7 +235,7 @@ export function callbackFactory(block: Block, xml: Element): () => void { eventUtils.disable(); let newBlock; try { - newBlock = Xml.domToBlock(xml, block.workspace!) as BlockSvg; + newBlock = Xml.domToBlockInternal(xml, block.workspace!) as BlockSvg; // Move the new block next to the old block. const xy = block.getRelativeToSurfaceXY(); if (block.RTL) { diff --git a/core/flyout_base.ts b/core/flyout_base.ts index 25fe446e6d7..8940574a658 100644 --- a/core/flyout_base.ts +++ b/core/flyout_base.ts @@ -36,6 +36,7 @@ import * as Variables from './variables.js'; import {WorkspaceSvg} from './workspace_svg.js'; import * as utilsXml from './utils/xml.js'; import * as Xml from './xml.js'; +import * as renderManagement from './render_management.js'; enum FlyoutItemType { BLOCK = 'block', @@ -626,6 +627,8 @@ export abstract class Flyout extends DeleteArea implements IFlyout { const parsedContent = toolbox.convertFlyoutDefToJsonArray(flyoutDef); const flyoutInfo = this.createFlyoutInfo(parsedContent); + renderManagement.triggerQueuedRenders(); + this.layout_(flyoutInfo.contents, flyoutInfo.gaps); if (this.horizontalLayout) { @@ -770,7 +773,7 @@ export abstract class Flyout extends DeleteArea implements IFlyout { ) as Element; block = this.getRecycledBlock(xml.getAttribute('type')!); if (!block) { - block = Xml.domToBlock(xml, this.workspace_); + block = Xml.domToBlockInternal(xml, this.workspace_); } } else { block = this.getRecycledBlock(blockInfo['type']!); @@ -779,7 +782,10 @@ export abstract class Flyout extends DeleteArea implements IFlyout { blockInfo['enabled'] = blockInfo['disabled'] !== 'true' && blockInfo['disabled'] !== true; } - block = blocks.append(blockInfo as blocks.State, this.workspace_); + block = blocks.appendInternal( + blockInfo as blocks.State, + this.workspace_ + ); } } diff --git a/core/serialization/blocks.ts b/core/serialization/blocks.ts index 23de10dbab3..d2d2857890a 100644 --- a/core/serialization/blocks.ts +++ b/core/serialization/blocks.ts @@ -18,6 +18,7 @@ import * as registry from '../registry.js'; import * as utilsXml from '../utils/xml.js'; import type {Workspace} from '../workspace.js'; import * as Xml from '../xml.js'; +import * as renderManagement from '../render_management.js'; import { BadConnectionCheck, @@ -349,7 +350,9 @@ export function append( workspace: Workspace, {recordUndo = false}: {recordUndo?: boolean} = {} ): Block { - return appendInternal(state, workspace, {recordUndo}); + const block = appendInternal(state, workspace, {recordUndo}); + if (workspace.rendered) renderManagement.triggerQueuedRenders(); + return block; } /** @@ -701,7 +704,9 @@ function initBlock(block: Block, rendered: boolean) { blockSvg.setConnectionTracking(false); blockSvg.initSvg(); - blockSvg.render(false); + blockSvg.queueRender(); + blockSvg.updateDisabled(); + // fixes #6076 JSO deserialization doesn't // set .iconXY_ property so here it will be set for (const icon of blockSvg.getIcons()) { diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 443249c8f1f..adb78ae6317 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -1349,7 +1349,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { let blockX = 0; let blockY = 0; if (xmlBlock) { - block = Xml.domToBlock(xmlBlock, this) as BlockSvg; + block = Xml.domToBlockInternal(xmlBlock, this) as BlockSvg; blockX = parseInt(xmlBlock.getAttribute('x') ?? '0'); if (this.RTL) { blockX = -blockX; diff --git a/core/xml.ts b/core/xml.ts index 0367e550a32..31df7804357 100644 --- a/core/xml.ts +++ b/core/xml.ts @@ -23,6 +23,7 @@ import type {Workspace} from './workspace.js'; import {WorkspaceComment} from './workspace_comment.js'; import {WorkspaceCommentSvg} from './workspace_comment_svg.js'; import type {WorkspaceSvg} from './workspace_svg.js'; +import * as renderManagement from './render_management.js'; /** * Encode a block tree as XML. @@ -430,7 +431,7 @@ export function domToWorkspace(xml: Element, workspace: Workspace): string[] { // Allow top-level shadow blocks if recordUndo is disabled since // that means an undo is in progress. Such a block is expected // to be moved to a nested destination in the next operation. - const block = domToBlock(xmlChildElement, workspace); + const block = domToBlockInternal(xmlChildElement, workspace); newBlockIds.push(block.id); const blockX = parseInt(xmlChildElement.getAttribute('x') ?? '10', 10); const blockY = parseInt(xmlChildElement.getAttribute('y') ?? '10', 10); @@ -467,12 +468,13 @@ export function domToWorkspace(xml: Element, workspace: Workspace): string[] { } } finally { eventUtils.setGroup(existingGroup); + if ((workspace as WorkspaceSvg).setResizesEnabled) { + (workspace as WorkspaceSvg).setResizesEnabled(true); + } + if (workspace.rendered) renderManagement.triggerQueuedRenders(); dom.stopTextWidthCache(); } // Re-enable workspace resizing. - if ((workspace as WorkspaceSvg).setResizesEnabled) { - (workspace as WorkspaceSvg).setResizesEnabled(true); - } eventUtils.fire(new (eventUtils.get(eventUtils.FINISHED_LOADING))(workspace)); return newBlockIds; } @@ -545,6 +547,27 @@ export function appendDomToWorkspace( * @returns The root block created. */ export function domToBlock(xmlBlock: Element, workspace: Workspace): Block { + const block = domToBlockInternal(xmlBlock, workspace); + if (workspace.rendered) renderManagement.triggerQueuedRenders(); + return block; +} + +/** + * Decode an XML block tag and create a block (and possibly sub blocks) on the + * workspace. + * + * This is defined internally so that it doesn't trigger an immediate render, + * which we do want to happen for external calls. + * + * @param xmlBlock XML block element. + * @param workspace The workspace. + * @returns The root block created. + * @internal + */ +export function domToBlockInternal( + xmlBlock: Element, + workspace: Workspace +): Block { // Create top-level block. eventUtils.disable(); const variablesBeforeCreation = workspace.getAllVariables(); @@ -561,7 +584,7 @@ export function domToBlock(xmlBlock: Element, workspace: Workspace): Block { (blocks[i] as BlockSvg).initSvg(); } for (let i = blocks.length - 1; i >= 0; i--) { - (blocks[i] as BlockSvg).render(false); + (blocks[i] as BlockSvg).queueRender(); } // Populating the connection database may be deferred until after the // blocks have rendered. diff --git a/tests/mocha/flyout_test.js b/tests/mocha/flyout_test.js index 0e6686b296e..5594f8e26c0 100644 --- a/tests/mocha/flyout_test.js +++ b/tests/mocha/flyout_test.js @@ -27,7 +27,7 @@ import { suite('Flyout', function () { setup(function () { - sharedTestSetup.call(this); + this.clock = sharedTestSetup.call(this, {fireEventsNow: false}).clock; Blockly.defineBlocksWithJsonArray([ { 'type': 'basic_block', @@ -48,6 +48,7 @@ suite('Flyout', function () { }); teardown(function () { + this.clock.runAll(); sharedTestTeardown.call(this); });