1111import * as Blockly from 'blockly/core' ;
1212import { Block } from 'blockly/core/block' ;
1313import { Abstract } from 'blockly/core/events/events_abstract' ;
14- import { BlockChangeJson } from 'blockly/core/events/events_block_change' ;
14+
15+ export interface BlockShadowStateChangeJson
16+ extends Blockly . Events . BlockBaseJson {
17+ inputIndexInParent : number | null ;
18+ shadowState : Blockly . serialization . blocks . State ;
19+ }
1520
1621/**
17- * A new blockly event class specifically for recording changes to the shadow
18- * state of a block. This implementation is similar to and could be merged with
19- * the implementation of Blockly.Events.BlockChange in Blockly core code .
22+ * A Blockly event class to revert a block connection's shadow state to the
23+ * provided state, to be used after attaching a child block that would
24+ * ordinarily overwrite the connection's shadow state .
2025 */
21- export class BlockShadowChange extends Blockly . Events . BlockBase {
26+ export class BlockShadowStateChange extends Blockly . Events . BlockBase {
2227 /**
2328 * The name of the event type for broadcast and listening purposes.
2429 */
2530 /* eslint-disable @typescript-eslint/naming-convention */
26- static readonly EVENT_TYPE = 'block_shadow_change ' ;
31+ static readonly EVENT_TYPE = 'block_shadow_state_change ' ;
2732 /* eslint-enable @typescript-eslint/naming-convention */
2833
2934 /**
30- * The previous value of the field.
35+ * The index of the connection in the parent block's list of connections. If
36+ * null, then the nextConnection will be used instead.
3137 */
32- oldValue : unknown ;
38+ inputIndexInParent : number | null ;
3339
3440 /**
35- * The new value of the field .
41+ * The intended shadow state of the connection .
3642 */
37- newValue : unknown ;
43+ shadowState : Blockly . serialization . blocks . State ;
3844
3945 /**
40- * The constructor for a new BlockShadowChange event.
46+ * The constructor for a new BlockShadowStateChange event.
4147 *
42- * @param block The changed block. Undefined for a blank event.
43- * @param oldValue Previous value of shadow state.
44- * @param newValue New value of shadow state.
48+ * @param block The parent of the connection to modify.
49+ * @param inputIndexInParent The index of the input associated with the
50+ * connection to modify, if it is associated with one. Otherwise the
51+ * nextConnection will be used.
52+ * @param shadowState The intended shadow state of the connection.
4553 */
46- constructor ( block ?: Block , oldValue ?: boolean , newValue ?: boolean ) {
54+ constructor (
55+ block : Block ,
56+ inputIndexInParent : number | null ,
57+ shadowState : Blockly . serialization . blocks . State ,
58+ ) {
4759 super ( block ) ;
4860
49- this . type = BlockShadowChange . EVENT_TYPE ;
50-
51- if ( ! block ) {
52- return ; // Blank event to be populated by fromJson.
53- }
54- this . oldValue = typeof oldValue === 'undefined' ? '' : oldValue ;
55- this . newValue = typeof newValue === 'undefined' ? '' : newValue ;
61+ this . type = BlockShadowStateChange . EVENT_TYPE ;
62+ this . inputIndexInParent = inputIndexInParent ;
63+ this . shadowState = shadowState ;
5664 }
5765
5866 /**
@@ -61,10 +69,10 @@ export class BlockShadowChange extends Blockly.Events.BlockBase {
6169 * @returns JSON representation.
6270 * @override
6371 */
64- toJson ( ) : BlockChangeJson {
65- const json = super . toJson ( ) as BlockChangeJson ;
66- json [ 'oldValue ' ] = this . oldValue ;
67- json [ 'newValue ' ] = this . newValue ;
72+ toJson ( ) : BlockShadowStateChangeJson {
73+ const json = super . toJson ( ) as BlockShadowStateChangeJson ;
74+ json [ 'inputIndexInParent ' ] = this . inputIndexInParent ;
75+ json [ 'shadowState ' ] = this . shadowState ;
6876 return json ;
6977 }
7078
@@ -75,18 +83,18 @@ export class BlockShadowChange extends Blockly.Events.BlockBase {
7583 * @override
7684 */
7785 static fromJson (
78- json : BlockChangeJson ,
86+ json : BlockShadowStateChangeJson ,
7987 workspace : Blockly . Workspace ,
8088 /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
8189 event ?: any ,
82- ) : BlockShadowChange {
90+ ) : BlockShadowStateChange {
8391 const newEvent = super . fromJson (
8492 json ,
8593 workspace ,
8694 event ,
87- ) as BlockShadowChange ;
88- newEvent . oldValue = json [ 'oldValue ' ] ;
89- newEvent . newValue = json [ 'newValue ' ] ;
95+ ) as BlockShadowStateChange ;
96+ newEvent . inputIndexInParent = json [ 'inputIndexInParent ' ] ;
97+ newEvent . shadowState = json [ 'shadowState ' ] ;
9098 return event ;
9199 }
92100
@@ -97,7 +105,7 @@ export class BlockShadowChange extends Blockly.Events.BlockBase {
97105 * @override
98106 */
99107 isNull ( ) : boolean {
100- return this . oldValue === this . newValue ;
108+ return false ;
101109 }
102110
103111 /**
@@ -122,17 +130,169 @@ export class BlockShadowChange extends Blockly.Events.BlockBase {
122130 ) ;
123131 }
124132
125- const value = forward ? this . newValue : this . oldValue ;
126- block . setShadow ( ! ! value ) ;
133+ const connections = block . getConnections_ ( true ) ;
134+
135+ let connection : Blockly . Connection | null ;
136+ if ( this . inputIndexInParent === null ) {
137+ connection = block . nextConnection ;
138+ } else if (
139+ typeof this . inputIndexInParent !== 'number' ||
140+ this . inputIndexInParent < 0 ||
141+ this . inputIndexInParent >= connections . length
142+ ) {
143+ throw new Error ( 'inputIndexInParent was invalid.' ) ;
144+ } else {
145+ connection = block . inputList [ this . inputIndexInParent ] . connection ;
146+ }
147+ if ( connection === null ) {
148+ throw new Error ( 'No matching connection was found.' ) ;
149+ }
150+
151+ if ( forward ) {
152+ connection . setShadowState ( this . shadowState || null ) ;
153+ }
154+
155+ // Nothing to be done when run backward, because removing a child block
156+ // doesn't overwrite the connection's shadowState and thus doesn't need to
157+ // be reverted.
127158 }
128159}
129160
130161Blockly . registry . register (
131162 Blockly . registry . Type . EVENT ,
132- BlockShadowChange . EVENT_TYPE ,
133- BlockShadowChange ,
163+ BlockShadowStateChange . EVENT_TYPE ,
164+ BlockShadowStateChange ,
134165) ;
135166
167+ /**
168+ * Convert the provided shadow block into a regular block, along with any parent
169+ * shadow blocks.
170+ *
171+ * The provided block will be deleted, and a new regular block will be created
172+ * in its place that has new id but is otherwise identical to the shadow block.
173+ * The parent connection's shadow state will be forcibly preserved, despite the
174+ * fact that attaching a regular block to the connection ordinarily overwrites
175+ * the connection's shadow state.
176+ *
177+ * @param shadowBlock
178+ * @returns The newly created regular block with a different id, if one could be
179+ * created.
180+ */
181+ function reifyEditedShadowBlock ( shadowBlock : Block ) : Blockly . Block {
182+ // Determine how the shadow block is connected to the parent.
183+ let parentConnection : Blockly . Connection | null = null ;
184+ let connectionIsThroughOutputConnection = false ;
185+ if ( shadowBlock . previousConnection ?. isConnected ( ) ) {
186+ parentConnection = shadowBlock . previousConnection . targetConnection ;
187+ } else if ( shadowBlock . outputConnection ?. isConnected ( ) ) {
188+ parentConnection = shadowBlock . outputConnection . targetConnection ;
189+ connectionIsThroughOutputConnection = true ;
190+ }
191+ if ( parentConnection === null ) {
192+ // We can't change the shadow status of a block with no parent, so just
193+ // return the block as-is.
194+ return shadowBlock ;
195+ }
196+
197+ // Get the parent block, and the index of the connection's input if it is
198+ // associated with one.
199+ let parentBlock = parentConnection . getSourceBlock ( ) ;
200+ const inputInParent = parentConnection . getParentInput ( ) ;
201+ const inputIndexInParent : number | null = inputInParent
202+ ? parentBlock . inputList . indexOf ( inputInParent )
203+ : null ;
204+
205+ // Recover the state of the shadow block before it was edited. The connection
206+ // should still have the original state until a new block is attached to it.
207+ const originalShadowState = parentConnection . getShadowState (
208+ /* returnCurrent = */ false ,
209+ ) ;
210+
211+ // Serialize the current state of the shadow block (after it was edited).
212+ const editedBlockState = Blockly . serialization . blocks . save ( shadowBlock , {
213+ addCoordinates : false ,
214+ addInputBlocks : true ,
215+ addNextBlocks : true ,
216+ doFullSerialization : false ,
217+ } ) ;
218+ if ( originalShadowState === null || editedBlockState === null ) {
219+ // The serialized block states are necessary to convert the block. Without
220+ // them, just return the block as-is.
221+ return shadowBlock ;
222+ }
223+
224+ // If the parent block is a shadow, it must be converted first.
225+ if ( parentBlock . isShadow ( ) ) {
226+ const newParentBlock = reifyEditedShadowBlock ( parentBlock ) ;
227+ if ( newParentBlock === null ) {
228+ throw new Error (
229+ "No parent block was created, so we can't recreate the " +
230+ 'current block either.' ,
231+ ) ;
232+ }
233+ parentBlock = newParentBlock ;
234+
235+ // The reference to the connection is obsolete. Find it from the new parent.
236+ if ( inputIndexInParent === null ) {
237+ parentConnection = parentBlock . nextConnection ;
238+ } else if (
239+ inputIndexInParent < 0 ||
240+ inputIndexInParent >= parentBlock . inputList . length
241+ ) {
242+ throw new Error ( 'inputIndexInParent is invalid.' ) ;
243+ } else {
244+ parentConnection = parentBlock . inputList [ inputIndexInParent ] . connection ;
245+ }
246+ if ( parentConnection === null ) {
247+ throw new Error (
248+ "Couldn't find the corresponding connection on the new " +
249+ 'version of the parent block.' ,
250+ ) ;
251+ }
252+ }
253+
254+ // Let Blockly generate a new id for the new regular block. Ideally, we would
255+ // let the shadow block and the regular block have the same id, and in
256+ // principle that ought to be possible since they don't need to coexist at the
257+ // same time. However, we'll need to call setShadowState on the connection
258+ // after attaching the regular block to revert any changes made by attaching
259+ // the block, and the setShadowState implementation temporarily instantiates
260+ // the provided shadow state, which can't have the same id as a block in the
261+ // workspace. The new shadow state id won't be compatible with any existing
262+ // undo history on the shadow block, such as the block change event that
263+ // triggered this whole shadow conversion!
264+ editedBlockState . id = undefined ;
265+
266+ // Create a regular version of the shadow block by deserializing its state
267+ // independently from the connection.
268+ const regularBlock = Blockly . serialization . blocks . append (
269+ editedBlockState ,
270+ parentBlock . workspace ,
271+ { recordUndo : true } ,
272+ ) ;
273+
274+ // Attach the regular block to the connection in place of the shadow block.
275+ const childConnection = connectionIsThroughOutputConnection
276+ ? regularBlock . outputConnection
277+ : regularBlock . previousConnection ;
278+ if ( childConnection ) {
279+ parentConnection . connect ( childConnection ) ;
280+ }
281+
282+ // The process of connecting a block overwrites the connection's shadow state,
283+ // so revert it.
284+ parentConnection . setShadowState ( originalShadowState ) ;
285+ Blockly . Events . fire (
286+ new BlockShadowStateChange (
287+ parentBlock ,
288+ inputIndexInParent ,
289+ originalShadowState ,
290+ ) ,
291+ ) ;
292+
293+ return regularBlock ;
294+ }
295+
136296/**
137297 * Add this function to your workspace as a change listener to automatically
138298 * convert shadow blocks to real blocks whenever the user edits a field on the
@@ -191,40 +351,7 @@ export function shadowBlockConversionChangeListener(event: Abstract) {
191351 Blockly . Events . setGroup ( true ) ;
192352 }
193353
194- // If the changed shadow block is a child of another shadow block, then both
195- // blocks should be converted to real blocks. To find all the shadow block
196- // ancestors that need to be converted to real blocks, seed the list of blocks
197- // starting with the changed block, and append all shadow block ancestors.
198- const shadowBlocks = [ block ] ;
199- for ( let i = 0 ; i < shadowBlocks . length ; i ++ ) {
200- const shadowBlock = shadowBlocks [ i ] ;
201-
202- // If connected blocks need to be converted too, add them to the list.
203- const outputBlock : Block | null | undefined =
204- shadowBlock . outputConnection ?. targetBlock ( ) ;
205- const previousBlock : Block | null | undefined =
206- shadowBlock . previousConnection ?. targetBlock ( ) ;
207- if ( outputBlock ?. isShadow ( ) ) {
208- shadowBlocks . push ( outputBlock ) ;
209- }
210- if ( previousBlock ?. isShadow ( ) ) {
211- shadowBlocks . push ( previousBlock ) ;
212- }
213- }
214-
215- // The list of shadow blocks starts with the deepest child and ends with the
216- // highest parent, but the parent of a real block should never be a shadow
217- // block, so the parents need to be converted to real blocks first. Start
218- // at the end of the list and iterate backward to convert the blocks.
219- for ( let i = shadowBlocks . length - 1 ; i >= 0 ; i -- ) {
220- const shadowBlock = shadowBlocks [ i ] ;
221- // Convert the shadow block to a real block and fire an event recording the
222- // change so that it can be undone. Ideally the
223- // Blockly.Block.prototype.setShadow method should fire this event directly,
224- // but for this plugin it needs to be explicitly fired here.
225- shadowBlock . setShadow ( false ) ;
226- Blockly . Events . fire ( new BlockShadowChange ( shadowBlock , true , false ) ) ;
227- }
354+ reifyEditedShadowBlock ( block ) ;
228355
229356 // Revert to the current event group, if any.
230357 Blockly . Events . setGroup ( currentGroup ) ;
0 commit comments