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.