@@ -267,138 +267,158 @@ Blockly.Blocks["arrays_range"] = {
267267} ;
268268
269269Blockly . Blocks [ 'arrays_expandablemake' ] = {
270- /**
271- * Block for creating an array with dynamic placeholders.
272- * Fixed to prevent shadow block ghosting during drags.
273- * @this Blockly.Block
274- */
275- init : function ( ) {
276- this . jsonInit ( {
277- "message0" : "array with %1 %2" ,
278- "args0" : [
279- {
280- "type" : "field_expandable_remove" ,
281- "name" : "REMOVE"
282- } ,
283- {
284- "type" : "field_expandable_add" ,
285- "name" : "ADD"
286- }
287- ] ,
288- "output" : "Array" ,
289- "extensions" : [ "colours_data_lists" , "shape_square" ]
290- } ) ;
270+ init : function ( ) {
271+ this . jsonInit ( {
272+ // We use a dummy input for the main label so we can change it later
273+ "message0" : "%2 %3 %1" ,
274+ "args0" : [
275+ {
276+ "type" : "field_label" ,
277+ "text" : "array" ,
278+ "name" : "MAIN_LABEL"
279+ } ,
280+ {
281+ "type" : "field_expandable_remove" ,
282+ "name" : "REMOVE"
283+ } ,
284+ {
285+ "type" : "field_expandable_add" ,
286+ "name" : "ADD"
287+ }
288+ ] ,
289+ "output" : "Array" ,
290+ "extensions" : [ "colours_data_lists" , "shape_square" ]
291+ } ) ;
291292
292- this . inputs_ = 0 ;
293- this . placeholders_ = [ "apple" , "banana" , "pear" , "orange" , "kiwi" ] ;
293+ this . placeholders_ = [ "apple" , "banana" , "pear" , "orange" , "kiwi" ] ;
294+ this . inputs_ = 0 ; // Start at 0 to keep it clean
295+ } ,
294296
295- // Initial setup
296- for ( let i = 0 ; i < 2 ; i ++ ) {
297- this . addInput_ ( i ) ;
298- this . inputs_ ++ ;
299- }
300- } ,
297+ /**
298+ * Internal helper to update labels and create/remove inputs.
299+ */
300+ updateShape_ : function ( targetCount , isLoading ) {
301+ const labelField = this . getField ( 'MAIN_LABEL' ) ;
302+ if ( labelField ) {
303+ labelField . setValue ( targetCount > 0 ? "array with" : Blockly . Msg . ARRAYS_EMPTY_ARRAY ) ;
304+ }
301305
302- /**
303- * Save the number of inputs to the XML.
304- */
305- mutationToDom : function ( ) {
306- const container = document . createElement ( 'mutation' ) ;
307- container . setAttribute ( 'items' , String ( this . inputs_ ) ) ;
308- return container ;
306+ // Add new inputs
307+ while ( this . inputs_ < targetCount ) {
308+ const index = this . inputs_ ;
309+ this . inputs_ ++ ;
310+ const inputName = `ADD${ index } ` ;
311+ const input = this . appendValueInput ( inputName ) ;
312+
313+ // CRITICAL FIX: Only fill shadows if NOT loading from XML
314+ if ( ! isLoading && ( this . rendered || this . isInFlyout ) ) {
315+ this . fillInBlock ( input . connection , index ) ;
316+ }
317+ }
318+
319+ // Remove inputs
320+ while ( this . inputs_ > targetCount ) {
321+ this . removeInput ( `ADD${ this . inputs_ - 1 } ` ) ;
322+ this . inputs_ -- ;
323+ }
309324 } ,
310325
311- /**
312- * Restore the number of inputs from XML.
313- */
314- domToMutation : function ( xmlElement ) {
315- const items = parseInt ( xmlElement . getAttribute ( 'items' ) , 10 ) ;
316- const newCount = isNaN ( items ) ? 0 : items ;
317-
318- // 1. Remove existing inputs to ensure a clean rebuild
319- for ( let i = 0 ; i < this . inputs_ ; i ++ ) {
320- if ( this . getInput ( 'ADD' + i ) ) {
321- this . removeInput ( 'ADD' + i ) ;
322- }
323- }
326+ fillInBlock : function ( connection , index ) {
327+ if ( connection . sourceBlock_ . isInsertionMarker_ ) return ;
324328
325- // 2. Update count and rebuild structure
326- this . inputs_ = newCount ;
327- for ( let i = 0 ; i < this . inputs_ ; i ++ ) {
328- this . addInput_ ( i ) ;
329- }
330- } ,
329+ const textValue = this . placeholders_ [ index ] || "item" ;
331330
332- /**
333- * Internal helper to create the input hole.
334- * Shadow blocks are only created when manually expanding, not during drag/load.
335- * @param {number } index The current input index.
336- * @private
337- */
338- addInput_ : function ( index ) {
339- const inputName = 'ADD' + index ;
340- if ( this . getInput ( inputName ) ) return ; // Prevent duplicate inputs
341-
342- const input = this . appendValueInput ( inputName ) ;
343- input . setCheck ( null ) ;
344-
345- // Logic check: Only spawn new shadow blocks if this is a live user action
346- // and NOT a re-rendering/dragging event (where recordUndo is usually false/null)
347- const isLiveAction = Blockly . Events . isEnabled ( ) && ! this . workspace . isFlyout ;
348-
349- if ( isLiveAction && this . workspace ) {
350- const placeholderText = this . placeholders_ [ index ] || "thing" ;
351- const conn = input . connection ;
352-
353- if ( conn && ! conn . targetConnection ) {
354- const shadowBlock = this . workspace . newBlock ( 'text' ) ;
355- shadowBlock . setShadow ( true ) ;
356- if ( shadowBlock . getField ( 'TEXT' ) ) {
357- shadowBlock . setFieldValue ( placeholderText , 'TEXT' ) ;
358- }
359- shadowBlock . initSvg ( ) ;
360- shadowBlock . render ( ) ;
361- conn . connect ( shadowBlock . outputConnection ) ;
362- }
331+ // 1. Create the XML definition of the shadow block
332+ const shadowDom = document . createElement ( "shadow" ) ;
333+ shadowDom . setAttribute ( "type" , "text" ) ;
334+
335+ const fieldDom = document . createElement ( "field" ) ;
336+ fieldDom . setAttribute ( "name" , "TEXT" ) ;
337+ fieldDom . textContent = textValue ;
338+
339+ shadowDom . appendChild ( fieldDom ) ;
340+
341+ // 2. Register the shadow XML directly to the connection
342+ connection . setShadowDom ( shadowDom ) ;
343+
344+ // 3. Spawn the shadow block visually
345+ if ( connection . respawnShadow_ ) {
346+ // The native Scratch-Blocks way to generate a shadow from its DOM
347+ connection . respawnShadow_ ( ) ;
348+ } else {
349+ // Fallback just in case respawnShadow_ is missing in your specific VM
350+ const shadowBlock = Blockly . Xml . domToBlock ( shadowDom , this . workspace ) ;
351+ shadowBlock . setShadow ( true ) ;
352+ shadowBlock . outputConnection . connect ( connection ) ;
353+ if ( this . rendered ) {
354+ shadowBlock . initSvg ( ) ;
355+ shadowBlock . render ( false ) ;
363356 }
357+ }
364358 } ,
365359
366- /**
367- * Triggered by FieldExpandable buttons.
368- */
369- onExpandableButtonClicked_ : function ( isAdding ) {
370- if ( this . workspace && this . workspace . isFlyout ) {
371- return ;
372- }
373- Blockly . Events . setGroup ( true ) ;
374- const oldMutation = Blockly . Xml . domToText ( this . mutationToDom ( ) ) ;
375-
376- if ( isAdding ) {
377- // Logic for adding a new input
378- this . addInput_ ( this . inputs_ ) ;
379- this . inputs_ ++ ;
380- } else {
381- // Logic for removing the last input
382- if ( this . inputs_ > 0 ) {
383- this . inputs_ -- ;
384- const inputName = 'ADD' + this . inputs_ ;
385- const input = this . getInput ( inputName ) ;
386- if ( input && input . connection && input . connection . targetBlock ( ) ) {
387- const target = input . connection . targetBlock ( ) ;
388- if ( target . isShadow ( ) ) {
389- target . dispose ( ) ;
390- }
391- }
392- this . removeInput ( inputName ) ;
360+ mutationToDom : function ( ) {
361+ const container = document . createElement ( "mutation" ) ;
362+ container . setAttribute ( "items" , String ( this . inputs_ ) ) ;
363+ return container ;
364+ } ,
365+
366+ domToMutation : function ( xmlElement ) {
367+ const items = parseInt ( xmlElement . getAttribute ( 'items' ) , 10 ) ;
368+ const newCount = isNaN ( items ) ? 0 : items ;
369+
370+ // Update label
371+ const labelField = this . getField ( 'MAIN_LABEL' ) ;
372+ if ( labelField ) {
373+ labelField . setValue ( newCount > 0 ? "array with" : Blockly . Msg . ARRAYS_EMPTY_ARRAY ) ;
374+ }
375+
376+ // Remove existing inputs (dispose any live shadows first)
377+ for ( let i = this . inputs_ - 1 ; i >= 0 ; i -- ) {
378+ const input = this . getInput ( 'ADD' + i ) ;
379+ if ( input && input . connection ) {
380+ const target = input . connection . targetBlock ( ) ;
381+ if ( target && target . isShadow ( ) ) {
382+ target . dispose ( false , false ) ;
393383 }
394384 }
385+ this . removeInput ( 'ADD' + i ) ;
386+ }
387+ this . inputs_ = 0 ;
395388
396- this . initSvg ( ) ;
397- this . render ( ) ;
389+ // Re-create inputs: register shadowDom for future respawning,
390+ // but do NOT call respawnShadow_() — the XML loader will attach
391+ // the saved block/shadow from the project file itself.
392+ for ( let i = 0 ; i < newCount ; i ++ ) {
393+ const input = this . appendValueInput ( 'ADD' + i ) ;
394+ this . inputs_ ++ ;
398395
399- const newMutation = Blockly . Xml . domToText ( this . mutationToDom ( ) ) ;
400- Blockly . Events . fire ( new Blockly . Events . BlockChange ( this ,
401- 'mutation' , null , oldMutation , newMutation ) ) ;
402- Blockly . Events . setGroup ( false ) ;
396+ // Register shadow DOM so it respawns when a real block is later removed
397+ const textValue = this . placeholders_ [ i ] || "item" ;
398+ const shadowDom = document . createElement ( "shadow" ) ;
399+ shadowDom . setAttribute ( "type" , "text" ) ;
400+ const fieldDom = document . createElement ( "field" ) ;
401+ fieldDom . setAttribute ( "name" , "TEXT" ) ;
402+ fieldDom . textContent = textValue ;
403+ shadowDom . appendChild ( fieldDom ) ;
404+ input . connection . setShadowDom ( shadowDom ) ;
405+ // ← No respawnShadow_() here! XML loader does the attachment.
403406 }
407+ } ,
408+
409+ onExpandableButtonClicked_ : function ( isAdding ) {
410+ Blockly . Events . setGroup ( true ) ;
411+ const oldMutation = Blockly . Xml . domToText ( this . mutationToDom ( ) ) ;
412+
413+ const newCount = isAdding ? this . inputs_ + 1 : Math . max ( 0 , this . inputs_ - 1 ) ;
414+ this . updateShape_ ( newCount ) ;
415+
416+ this . initSvg ( ) ;
417+ if ( this . rendered ) this . render ( ) ;
418+
419+ const newMutation = Blockly . Xml . domToText ( this . mutationToDom ( ) ) ;
420+ Blockly . Events . fire ( new Blockly . Events . BlockChange ( this ,
421+ 'mutation' , null , oldMutation , newMutation ) ) ;
422+ Blockly . Events . setGroup ( false ) ;
423+ }
404424} ;
0 commit comments