Skip to content

Commit f37b209

Browse files
authored
revert "fix: Use setShadowState instead of setShadow in shadow-block-converter" (#2149)
1 parent 0ee8651 commit f37b209

File tree

4 files changed

+162
-364
lines changed

4 files changed

+162
-364
lines changed

plugins/shadow-block-converter/README.md

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# @blockly/shadow-block-converter [![Built on Blockly](https://tinyurl.com/built-on-blockly)](https://github.com/google/blockly)
22

3-
A [Blockly](https://www.npmjs.com/package/blockly) plugin for automatically converting shadow blocks to regular blocks when the user edits them.
3+
A [Blockly](https://www.npmjs.com/package/blockly) plugin for automatically converting shadow blocks to real blocks when the user edits them.
44

55
## Installation
66

@@ -19,24 +19,9 @@ npm install @blockly/shadow-block-converter --save
1919
## Usage
2020

2121
This plugin exports a function called `shadowBlockConversionChangeListener`. If
22-
you add it as a change listener to your Blockly workspace then any shadow block
23-
the user edits will be converted to a regular block. This allows the user to
24-
move or delete the block, in which case the original shadow block will
25-
automatically return. With this plugin, shadow blocks behave like a persistent
26-
default value associated with the parent block (unlike standard Blockly
27-
behavior, where shadow blocks retain any edits made to them even after a regular
28-
block is dragged on top of them).
29-
30-
The regular block will be a new block instance, separate from the shadow block
31-
that was replaced, and will have a different id. It will otherwise have the same
32-
properties and shape as the original shadow block.
33-
34-
If the shadow block was attached to any ancestor blocks that were also shadows,
35-
they will be recreated as regular blocks. If the shadow block was attached to
36-
any descendent blocks, they will be recreated with different ids but will still
37-
be shadow blocks.
38-
39-
See below for an example using it with a workspace.
22+
you add it as a change listener to your blockly workspace then any shadow block
23+
the user edits will be converted to a real block. See below for an example using
24+
it with a workspace.
4025

4126
### JavaScript
4227

plugins/shadow-block-converter/src/shadow_block_converter.ts

Lines changed: 70 additions & 197 deletions
Original file line numberDiff line numberDiff line change
@@ -11,56 +11,48 @@
1111
import * as Blockly from 'blockly/core';
1212
import {Block} from 'blockly/core/block';
1313
import {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

161130
Blockly.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);

plugins/shadow-block-converter/test/index.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,6 @@ import {shadowBlockConversionChangeListener} from '../src/index';
1515
const toolbox: Blockly.utils.toolbox.ToolboxDefinition = {
1616
kind: 'flyoutToolbox',
1717
contents: [
18-
{
19-
kind: 'block',
20-
type: 'text_reverse',
21-
inputs: {
22-
TEXT: {
23-
shadow: {type: 'text', fields: {TEXT: 'abc'}},
24-
block: undefined,
25-
},
26-
},
27-
},
2818
{
2919
kind: 'block',
3020
type: 'colour_blend',

0 commit comments

Comments
 (0)