From fbd860184f9d075239eb4ba2d960bb4b9c454a7b Mon Sep 17 00:00:00 2001 From: Richard Knoll Date: Tue, 10 Dec 2024 09:37:41 -0800 Subject: [PATCH 1/4] fix extra render trigger when initializing expandable blocks (#10312) --- pxtblocks/composableMutations.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pxtblocks/composableMutations.ts b/pxtblocks/composableMutations.ts index 276f98d5c21b..d7a53d60e839 100644 --- a/pxtblocks/composableMutations.ts +++ b/pxtblocks/composableMutations.ts @@ -345,9 +345,18 @@ export function initExpandableBlock(info: pxtc.BlocksInfo, b: Blockly.Block, def Blockly.Events.disable(); try { - const nb = Blockly.Xml.domToBlock(shadow, b.workspace); - if (nb) { - input.connection.connect(nb.outputConnection); + let newBlock: Blockly.Block; + if (!b.initialized) { + // use domToBlockInternal so that we don't trigger a render while + // the block is still being initialized + newBlock = Blockly.Xml.domToBlockInternal(shadow, b.workspace); + } + else { + newBlock = Blockly.Xml.domToBlock(shadow, b.workspace); + } + + if (newBlock) { + input.connection.connect(newBlock.outputConnection); } } catch (e) { } From 21dac27a6146e7055bd839e22553b186aedd857a Mon Sep 17 00:00:00 2001 From: Richard Knoll Date: Fri, 17 Jan 2025 10:46:04 -0800 Subject: [PATCH 2/4] clean up shadow blocks in insertion markers for expandable blocks (#10315) --- pxtblocks/composableMutations.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pxtblocks/composableMutations.ts b/pxtblocks/composableMutations.ts index d7a53d60e839..d3f0e52b8d74 100644 --- a/pxtblocks/composableMutations.ts +++ b/pxtblocks/composableMutations.ts @@ -350,6 +350,17 @@ export function initExpandableBlock(info: pxtc.BlocksInfo, b: Blockly.Block, def // use domToBlockInternal so that we don't trigger a render while // the block is still being initialized newBlock = Blockly.Xml.domToBlockInternal(shadow, b.workspace); + + // we don't know at this time whether the parent block is an insertion marker + // or not. doing this check lets us clean up the block in the case that it is, + // though we get an annoying flicker + setTimeout(() => { + if (newBlock.isInsertionMarker()) { + Blockly.Events.disable(); + newBlock.dispose(); + Blockly.Events.enable(); + } + }) } else { newBlock = Blockly.Xml.domToBlock(shadow, b.workspace); From 0fda63df0466b0be7d738f2493ad5dd703efd861 Mon Sep 17 00:00:00 2001 From: Richard Knoll Date: Fri, 17 Jan 2025 10:03:32 -0800 Subject: [PATCH 3/4] move github init after app target setup (#10284) --- webapp/src/app.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webapp/src/app.tsx b/webapp/src/app.tsx index eab841f53d32..6c0f5599543b 100644 --- a/webapp/src/app.tsx +++ b/webapp/src/app.tsx @@ -5963,11 +5963,12 @@ document.addEventListener("DOMContentLoaded", async () => { pxt.blocks.showBlockIdInTooltip = true; } - initGitHubDb(); pxt.perf.measureStart("setAppTarget"); pkg.setupAppTarget((window as any).pxtTargetBundle); + initGitHubDb(); + // DO NOT put any async code before this line! The serviceworker must be initialized before // the window load event fires appcache.init(() => theEditor.reloadEditor()); From 8e16ac3c9bdc3c3056817f2113f30da44c7a404a Mon Sep 17 00:00:00 2001 From: Richard Knoll Date: Fri, 17 Jan 2025 10:45:37 -0800 Subject: [PATCH 4/4] new strategy for duplicate on drag blocks (#10343) --- pxtblocks/builtins/loops.ts | 3 + pxtblocks/loader.ts | 7 +- .../duplicateOnDrag/connectionChecker.ts | 4 +- .../plugins/duplicateOnDrag/dragStrategy.ts | 4 +- .../duplicateOnDrag/duplicateOnDrag.ts | 70 ++++++++++++++++++- .../blocks/functionDefinitionBlock.ts | 4 +- 6 files changed, 84 insertions(+), 8 deletions(-) diff --git a/pxtblocks/builtins/loops.ts b/pxtblocks/builtins/loops.ts index 1b70cfdd6a0a..6eb52bd4b7c1 100644 --- a/pxtblocks/builtins/loops.ts +++ b/pxtblocks/builtins/loops.ts @@ -2,6 +2,7 @@ import * as Blockly from "blockly"; import { installBuiltinHelpInfo, setBuiltinHelpInfo, setHelpResources } from "../help"; +import { setDuplicateOnDrag } from "../plugins/duplicateOnDrag"; export function initLoops() { const msg = Blockly.Msg; @@ -104,6 +105,7 @@ export function initLoops() { } } }; + setDuplicateOnDrag(pxtControlsForId, "VAR"); // controls_simple_for const controlsSimpleForId = "controls_simple_for"; @@ -313,6 +315,7 @@ export function initLoops() { ); } }; + setDuplicateOnDrag(pxtControlsForOfId, "VAR"); // controls_for_of const controlsForOfId = "controls_for_of"; diff --git a/pxtblocks/loader.ts b/pxtblocks/loader.ts index 4dd518c00d80..f946145c4a8b 100644 --- a/pxtblocks/loader.ts +++ b/pxtblocks/loader.ts @@ -23,7 +23,7 @@ import { initOnStart } from "./builtins/misc"; import { initContextMenu } from "./contextMenu"; import { renderCodeCard } from "./codecardRenderer"; import { FieldDropdown } from "./fields/field_dropdown"; -import { setDraggableShadowBlocks, setDuplicateOnDragStrategy } from "./plugins/duplicateOnDrag"; +import { setDraggableShadowBlocks, setDuplicateOnDrag, setDuplicateOnDragStrategy } from "./plugins/duplicateOnDrag"; import { applyPolyfills } from "./polyfills"; @@ -259,6 +259,11 @@ function initBlock(block: Blockly.Block, info: pxtc.BlocksInfo, fn: pxtc.SymbolI } else { i.setCheck("Variable"); } + + }); + + comp.handlerArgs.forEach(arg => { + setDuplicateOnDrag(block.type, "HANDLER_DRAG_PARAM_" + arg.name); }); } else { diff --git a/pxtblocks/plugins/duplicateOnDrag/connectionChecker.ts b/pxtblocks/plugins/duplicateOnDrag/connectionChecker.ts index 89983500234b..558095807492 100644 --- a/pxtblocks/plugins/duplicateOnDrag/connectionChecker.ts +++ b/pxtblocks/plugins/duplicateOnDrag/connectionChecker.ts @@ -1,5 +1,5 @@ import * as Blockly from "blockly"; -import { isDuplicateOnDragBlock } from "./duplicateOnDrag"; +import { shouldDuplicateOnDrag } from "./duplicateOnDrag"; const OPPOSITE_TYPE: number[] = []; @@ -16,7 +16,7 @@ export class DuplicateOnDragConnectionChecker extends Blockly.ConnectionChecker const replacedBlock = b.targetBlock(); - if (replacedBlock && isDuplicateOnDragBlock(replacedBlock)) return false; + if (replacedBlock && shouldDuplicateOnDrag(replacedBlock)) return false; return true; } diff --git a/pxtblocks/plugins/duplicateOnDrag/dragStrategy.ts b/pxtblocks/plugins/duplicateOnDrag/dragStrategy.ts index 1d05382535e1..6f5c937f484a 100644 --- a/pxtblocks/plugins/duplicateOnDrag/dragStrategy.ts +++ b/pxtblocks/plugins/duplicateOnDrag/dragStrategy.ts @@ -6,7 +6,7 @@ import * as Blockly from "blockly"; -import { DUPLICATE_ON_DRAG_MUTATION_KEY, isAllowlistedShadow } from "./duplicateOnDrag"; +import { DUPLICATE_ON_DRAG_MUTATION_KEY, isAllowlistedShadow, shouldDuplicateOnDrag } from "./duplicateOnDrag"; import eventUtils = Blockly.Events; import Coordinate = Blockly.utils.Coordinate; import dom = Blockly.utils.dom; @@ -160,7 +160,7 @@ export class DuplicateOnDragStrategy implements Blockly.IDragStrategy { const mutation = this.block.mutationToDom?.(); - if (mutation?.getAttribute(DUPLICATE_ON_DRAG_MUTATION_KEY)?.toLowerCase() === "true" || (isAllowlistedShadow(this.block) && isShadow)) { + if (shouldDuplicateOnDrag(this.block)) { const output = this.block.outputConnection; if (!output?.targetConnection) return; diff --git a/pxtblocks/plugins/duplicateOnDrag/duplicateOnDrag.ts b/pxtblocks/plugins/duplicateOnDrag/duplicateOnDrag.ts index 4b8d9aff2155..2d43955a33cb 100644 --- a/pxtblocks/plugins/duplicateOnDrag/duplicateOnDrag.ts +++ b/pxtblocks/plugins/duplicateOnDrag/duplicateOnDrag.ts @@ -3,21 +3,87 @@ import * as Blockly from "blockly"; export const DUPLICATE_ON_DRAG_MUTATION_KEY = "duplicateondrag"; let draggableShadowAllowlist: string[]; +let duplicateRefs: DuplicateOnDragRef[]; -export function isDuplicateOnDragBlock(block: Blockly.Block) { - return block.mutationToDom?.()?.getAttribute(DUPLICATE_ON_DRAG_MUTATION_KEY)?.toLowerCase() === "true"; +interface DuplicateOnDragRef { + parentBlockType: string; + inputName?: string; + childBlockType?: string; } export function setDraggableShadowBlocks(ids: string[]) { draggableShadowAllowlist = ids; } +/** + * Configures duplicate on drag for a block's child inputs + * + * @param parentBlockType The type of the parent block + * @param inputName The value input to duplicate blocks on when dragged. If not + * specified, all child value inputs will be duplicated + * @param childBlockType The type of the child block to be duplicated. If not specified, + * any block attached to the input will be duplicated on drag + * regardless of type + */ +export function setDuplicateOnDrag(parentBlockType: string, inputName?: string, childBlockType?: string) { + if (!duplicateRefs) { + duplicateRefs = []; + } + + const existing = duplicateRefs.some(ref => ref.parentBlockType === parentBlockType && ref.inputName === inputName && ref.childBlockType === childBlockType); + if (existing) { + return; + } + + duplicateRefs.push({ + parentBlockType, + inputName, + childBlockType + }); +} + export function isAllowlistedShadow(block: Blockly.Block) { if (draggableShadowAllowlist) { if (draggableShadowAllowlist.indexOf(block.type) !== -1) { return true; } } + return false; +} + +export function shouldDuplicateOnDrag(block: Blockly.Block) { + if (block.isShadow() && isAllowlistedShadow(block)) { + return true; + } + + if (duplicateRefs) { + const parent = block.outputConnection?.targetBlock(); + + if (parent) { + const refs = duplicateRefs.filter(r => r.parentBlockType === parent.type); + + for (const ref of refs) { + if (ref && (!ref.childBlockType || ref.childBlockType === block.type)) { + if (ref.inputName) { + const targetConnection = block.outputConnection.targetConnection; + if (targetConnection.getParentInput().name === ref.inputName) { + return true; + } + } + else { + return true; + } + } + } + } + } + + if (block.mutationToDom) { + const mutation = block.mutationToDom(); + if (mutation?.getAttribute(DUPLICATE_ON_DRAG_MUTATION_KEY)?.toLowerCase() === "true") { + return true; + } + } return false; } \ No newline at end of file diff --git a/pxtblocks/plugins/functions/blocks/functionDefinitionBlock.ts b/pxtblocks/plugins/functions/blocks/functionDefinitionBlock.ts index d5fd4f48a8ab..94a30c7c2295 100644 --- a/pxtblocks/plugins/functions/blocks/functionDefinitionBlock.ts +++ b/pxtblocks/plugins/functions/blocks/functionDefinitionBlock.ts @@ -21,7 +21,7 @@ import { MsgKey } from "../msg"; import { FunctionManager } from "../functionManager"; import { COLLAPSE_IMAGE_DATAURI } from "../svgs"; import { ArgumentReporterBlock } from "./argumentReporterBlocks"; -import { DUPLICATE_ON_DRAG_MUTATION_KEY } from "../../duplicateOnDrag"; +import { DUPLICATE_ON_DRAG_MUTATION_KEY, setDuplicateOnDrag } from "../../duplicateOnDrag"; interface FunctionDefinitionMixin extends CommonFunctionMixin { createArgumentReporter_(arg: FunctionArgument): ArgumentReporterBlock; @@ -205,6 +205,8 @@ Blockly.Blocks[FUNCTION_DEFINITION_BLOCK_TYPE] = { }, }; +setDuplicateOnDrag(FUNCTION_DEFINITION_BLOCK_TYPE); + function editFunctionCallback(block: CommonFunctionBlock) { // Edit can come from either the function definition or a function call.