1111import * as Blockly from 'blockly/core' ;
1212import { Block } from 'blockly/core/block' ;
1313import { Abstract } from 'blockly/core/events/events_abstract' ;
14-
15- export interface BlockShadowStateChangeJson
16- extends Blockly . Events . BlockBaseJson {
17- inputIndexInParent : number | null ;
18- shadowState : Blockly . serialization . blocks . State ;
19- }
14+ import { BlockChangeJson } from 'blockly/core/events/events_block_change' ;
2015
2116/**
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 .
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 .
2520 */
26- export class BlockShadowStateChange extends Blockly . Events . BlockBase {
21+ export class BlockShadowChange extends Blockly . Events . BlockBase {
2722 /**
2823 * The name of the event type for broadcast and listening purposes.
2924 */
3025 /* eslint-disable @typescript-eslint/naming-convention */
31- static readonly EVENT_TYPE = 'block_shadow_state_change ' ;
26+ static readonly EVENT_TYPE = 'block_shadow_change ' ;
3227 /* eslint-enable @typescript-eslint/naming-convention */
3328
3429 /**
35- * The index of the connection in the parent block's list of connections. If
36- * null, then the nextConnection will be used instead.
30+ * The previous value of the field.
3731 */
38- inputIndexInParent : number | null ;
32+ oldValue : unknown ;
3933
4034 /**
41- * The intended shadow state of the connection .
35+ * The new value of the field .
4236 */
43- shadowState : Blockly . serialization . blocks . State ;
37+ newValue : unknown ;
4438
4539 /**
46- * The constructor for a new BlockShadowStateChange event.
40+ * The constructor for a new BlockShadowChange event.
4741 *
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.
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.
5345 */
54- constructor (
55- block : Block ,
56- inputIndexInParent : number | null ,
57- shadowState : Blockly . serialization . blocks . State ,
58- ) {
46+ constructor ( block ?: Block , oldValue ?: boolean , newValue ?: boolean ) {
5947 super ( block ) ;
6048
61- this . type = BlockShadowStateChange . EVENT_TYPE ;
62- this . inputIndexInParent = inputIndexInParent ;
63- this . shadowState = shadowState ;
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 ;
6456 }
6557
6658 /**
@@ -69,10 +61,10 @@ export class BlockShadowStateChange extends Blockly.Events.BlockBase {
6961 * @returns JSON representation.
7062 * @override
7163 */
72- toJson ( ) : BlockShadowStateChangeJson {
73- const json = super . toJson ( ) as BlockShadowStateChangeJson ;
74- json [ 'inputIndexInParent ' ] = this . inputIndexInParent ;
75- json [ 'shadowState ' ] = this . shadowState ;
64+ toJson ( ) : BlockChangeJson {
65+ const json = super . toJson ( ) as BlockChangeJson ;
66+ json [ 'oldValue ' ] = this . oldValue ;
67+ json [ 'newValue ' ] = this . newValue ;
7668 return json ;
7769 }
7870
@@ -83,18 +75,18 @@ export class BlockShadowStateChange extends Blockly.Events.BlockBase {
8375 * @override
8476 */
8577 static fromJson (
86- json : BlockShadowStateChangeJson ,
78+ json : BlockChangeJson ,
8779 workspace : Blockly . Workspace ,
8880 /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
8981 event ?: any ,
90- ) : BlockShadowStateChange {
82+ ) : BlockShadowChange {
9183 const newEvent = super . fromJson (
9284 json ,
9385 workspace ,
9486 event ,
95- ) as BlockShadowStateChange ;
96- newEvent . inputIndexInParent = json [ 'inputIndexInParent ' ] ;
97- newEvent . shadowState = json [ 'shadowState ' ] ;
87+ ) as BlockShadowChange ;
88+ newEvent . oldValue = json [ 'oldValue ' ] ;
89+ newEvent . newValue = json [ 'newValue ' ] ;
9890 return event ;
9991 }
10092
@@ -105,7 +97,7 @@ export class BlockShadowStateChange extends Blockly.Events.BlockBase {
10597 * @override
10698 */
10799 isNull ( ) : boolean {
108- return false ;
100+ return this . oldValue === this . newValue ;
109101 }
110102
111103 /**
@@ -130,169 +122,17 @@ export class BlockShadowStateChange extends Blockly.Events.BlockBase {
130122 ) ;
131123 }
132124
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.
125+ const value = forward ? this . newValue : this . oldValue ;
126+ block . setShadow ( ! ! value ) ;
158127 }
159128}
160129
161130Blockly . registry . register (
162131 Blockly . registry . Type . EVENT ,
163- BlockShadowStateChange . EVENT_TYPE ,
164- BlockShadowStateChange ,
132+ BlockShadowChange . EVENT_TYPE ,
133+ BlockShadowChange ,
165134) ;
166135
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-
296136/**
297137 * Add this function to your workspace as a change listener to automatically
298138 * convert shadow blocks to real blocks whenever the user edits a field on the
@@ -351,7 +191,40 @@ export function shadowBlockConversionChangeListener(event: Abstract) {
351191 Blockly . Events . setGroup ( true ) ;
352192 }
353193
354- reifyEditedShadowBlock ( block ) ;
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+ }
355228
356229 // Revert to the current event group, if any.
357230 Blockly . Events . setGroup ( currentGroup ) ;
0 commit comments