@@ -79,13 +79,20 @@ class DelegateToParentDraggable implements Blockly.IDraggable {
7979}
8080
8181/**
82- * Class representing a draggable block that copies itself on drag.
82+ * Class representing a draggable block that copies itself on drag, but only
83+ * when the block is directly connected to a procedures_prototype block.
84+ * When dragged from any other position, it behaves like a normal block.
8385 */
8486class DuplicateOnDragDraggable implements Blockly . IDraggable {
8587 /**
86- * The newly-created duplicate block.
88+ * The block being dragged: a newly-created duplicate when dragging from a
89+ * prototype, or the original block when dragging from elsewhere.
8790 */
8891 private copy ?: Blockly . BlockSvg
92+ /**
93+ * Whether this drag is duplicating the block (true) or moving it (false).
94+ */
95+ private isDuplicating_ = false
8996 constructor ( private block : Blockly . BlockSvg ) { }
9097
9198 /**
@@ -97,23 +104,37 @@ class DuplicateOnDragDraggable implements Blockly.IDraggable {
97104 }
98105
99106 /**
100- * Handles the start of a drag.
107+ * Handles the start of a drag. If the block is directly connected to a
108+ * procedures_prototype, creates a duplicate and drags that. Otherwise,
109+ * switches to a normal drag strategy and drags the original block.
101110 * @param e The event that triggered the drag.
102111 */
103112 startDrag ( e : PointerEvent ) {
104- const data = this . block . toCopyData ( )
105- if ( ! data ) {
106- console . warn (
107- 'DuplicateOnDragDraggable.startDrag: failed to serialize block for copy' ,
108- this . block . type ,
109- this . block . id ,
110- )
111- return
113+ const parent = this . block . getParent ( )
114+ this . isDuplicating_ = parent ?. type === 'procedures_prototype'
115+
116+ if ( this . isDuplicating_ ) {
117+ const data = this . block . toCopyData ( )
118+ if ( ! data ) {
119+ console . warn (
120+ 'DuplicateOnDragDraggable.startDrag: failed to serialize block for copy' ,
121+ this . block . type ,
122+ this . block . id ,
123+ )
124+ return
125+ }
126+ this . copy = Blockly . clipboard . paste ( data , this . block . workspace ) as Blockly . BlockSvg
127+ this . copy . setDeletable ( true )
128+ this . copy . setDragStrategy ( new Blockly . dragging . BlockDragStrategy ( this . copy ) )
129+ this . copy . startDrag ( e )
130+ } else {
131+ // Not in a prototype: drag the original block normally and replace this
132+ // drag strategy so future drags also behave normally.
133+ const normalStrategy = new Blockly . dragging . BlockDragStrategy ( this . block )
134+ this . block . setDragStrategy ( normalStrategy )
135+ this . copy = this . block
136+ normalStrategy . startDrag ( e )
112137 }
113- this . copy = Blockly . clipboard . paste ( data , this . block . workspace ) as Blockly . BlockSvg
114- this . copy . setDeletable ( true )
115- this . copy . setDragStrategy ( new Blockly . dragging . BlockDragStrategy ( this . copy ) )
116- this . copy . startDrag ( e )
117138 }
118139
119140 drag ( newLoc : Blockly . utils . Coordinate , e ?: PointerEvent ) {
@@ -135,7 +156,11 @@ class DuplicateOnDragDraggable implements Blockly.IDraggable {
135156 }
136157
137158 revertDrag ( ) {
138- this . copy ?. dispose ( )
159+ if ( this . isDuplicating_ ) {
160+ this . copy ?. dispose ( )
161+ } else {
162+ this . copy ?. revertDrag ( )
163+ }
139164 }
140165
141166 getRelativeToSurfaceXY ( ) {
@@ -210,7 +235,31 @@ function definitionDomToMutation(this: ProcedurePrototypeBlock | ProcedureDeclar
210235 this . argumentIds_ = JSON . parse ( xmlElement . getAttribute ( 'argumentids' ) ! )
211236 this . displayNames_ = JSON . parse ( xmlElement . getAttribute ( 'argumentnames' ) ! )
212237 this . argumentDefaults_ = JSON . parse ( xmlElement . getAttribute ( 'argumentdefaults' ) ! )
238+
239+ // During full XML deserialization (Blockly.Xml.domToWorkspace), the mutation element
240+ // is part of the parsed XML tree and its parent element also contains <value> children
241+ // for the argument reporters. Blockly will connect those child blocks AFTER the
242+ // mutation runs. To avoid creating duplicate reporters here that would immediately
243+ // be orphaned when the XML children connect, skip reporter creation and let the
244+ // XML children provide them.
245+ // We detect this case by checking that the parent element has <value> children.
246+ // This distinguishes full deserialization from:
247+ // - Programmatic mutation (xmlElement.parentElement is null — freshly created element)
248+ // - Block creation from partial XML with no <value> children (e.g. createProcedureCallbackFactory)
249+ // - JSON serialization / undo-redo (xmlElement.parentElement is null — textToDom result)
250+ const xmlParent = xmlElement . parentElement
251+ const hasXmlArgReporters =
252+ this . type === 'procedures_prototype' &&
253+ xmlParent !== null &&
254+ Array . from ( xmlParent . children ) . some ( ( el ) => el . tagName . toLowerCase ( ) === 'value' )
255+ if ( hasXmlArgReporters ) {
256+ ; ( this as ProcedurePrototypeBlock ) . skipArgumentReporters_ = true
257+ }
213258 this . updateDisplay_ ( )
259+ if ( hasXmlArgReporters ) {
260+ ; ( this as ProcedurePrototypeBlock ) . skipArgumentReporters_ = false
261+ }
262+
214263 if ( 'updateArgumentReporterNames_' in this ) {
215264 this . updateArgumentReporterNames_ ( prevArgIds , prevDisplayNames )
216265 }
@@ -521,6 +570,12 @@ function populateArgumentOnPrototype_(
521570 id : string ,
522571 input : Blockly . Input ,
523572) {
573+ // During XML deserialization, skip connecting argument reporters here.
574+ // The block's <value> XML children will connect the reporters after the mutation runs.
575+ if ( this . skipArgumentReporters_ ) {
576+ return
577+ }
578+
524579 let oldBlock : Blockly . BlockSvg | null = null
525580 if ( connectionMap && id in connectionMap ) {
526581 const saveInfo = connectionMap [ id ]
@@ -949,6 +1004,7 @@ Blockly.Blocks.procedures_prototype = {
9491004 this . argumentIds_ = [ ]
9501005 this . argumentDefaults_ = [ ]
9511006 this . warp_ = false
1007+ this . skipArgumentReporters_ = false
9521008
9531009 // Shared.
9541010 this . getProcCode = getProcCode . bind ( this )
@@ -1139,6 +1195,7 @@ interface ProcedureCallBlock extends ProcedureBlock {
11391195interface ProcedurePrototypeBlock extends ProcedureBlock {
11401196 displayNames_ : string [ ]
11411197 argumentDefaults_ : string [ ]
1198+ skipArgumentReporters_ : boolean
11421199 createArgumentReporter_ : ( argumentType : ArgumentType , displayName : string ) => Blockly . BlockSvg
11431200 updateArgumentReporterNames_ : ( prevArgIds : string [ ] , prevDisplayNames : string [ ] ) => void
11441201}
0 commit comments