Ability to remove mutations on Copy

107 views
Skip to first unread message

Eric Edem

unread,
Jul 6, 2021, 4:42:38 PM7/6/21
to Blockly
We have a use case where one of our blocks represents a system entity that has an ID. We track this through our system by adding a mutation to the block with an ID. This block has statements under it and forms a sort of "function" or "routine".

This works great but the problem is when someone wants to copy the blocks. Everything gets copied including this ID mutation. Is there a way to strip out this mutation when copying (or when pasting) this set of blocks?

We found this method, but I didn't see a clean way to inject the code needed to accomplish the above: https://github.com/google/blockly/blob/master/core/block_svg.js#L958

Best,
Eric

Beka Westberg

unread,
Jul 6, 2021, 7:03:51 PM7/6/21
to blo...@googlegroups.com
Hello Eric,

Sadly I don't think there's a good hook for this at the moment :/

One option is to disable copying of that top-level block. To do this you'll have to unregister the shortcut key and the context menu item, then re-add them with new precondition functions. Here are the context menu docs. For keyboard shortcuts there's a tiny bit of docs, but it's probably better to just look at the source code (api source, built-in shortcuts).
The other option is to monkey patch the function you found, but I don't recommend that because it could break at any time in the future.

Oh! And if you have time, it might be worth filing a feature request to try to get this added to core =)

I hope that helps! If you have any further questions please reply!
--Beka

--
You received this message because you are subscribed to the Google Groups "Blockly" group.
To unsubscribe from this group and stop receiving emails from it, send an email to blockly+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/blockly/93423969-90bd-49f9-b654-534e81a867c6n%40googlegroups.com.

Coda Highland

unread,
Jul 6, 2021, 7:56:10 PM7/6/21
to blo...@googlegroups.com
Well, here's an idea: Pasting a block should trigger a change event, right? So in the handler, detect duplicated IDs and fix the newly-added block.

/s/ Adam

Beka Westberg

unread,
Jul 7, 2021, 9:57:02 AM7/7/21
to blo...@googlegroups.com
That's a clever idea Adam! Sounds like it would definitely work. The specific kind of event you'd want to listen for is a create event.

--Beka

Eric Edem

unread,
Jul 7, 2021, 1:52:29 PM7/7/21
to Blockly
Cool idea Adam! If I understand correctly, I think it may not work for us. This ID we want to wipe out is across different workspaces, so we wouldn't be able to verify if someone copied from one workspace to another as there would be no collision of the ID.

I've added a feature request Beka, thanks for your suggestions. For now, I think we will avoid some of our system issues by just disabling copying of this block as that will get us by.

The feature request for reference: https://github.com/google/blockly/issues/4996

Coda Highland

unread,
Jul 7, 2021, 2:37:48 PM7/7/21
to blo...@googlegroups.com
Well, what's your logic for assigning them? Perhaps it's just a matter of ALWAYS clearing the ID when a new block is created, then query for the new ID?

/s/ Adam

Eric Edem

unread,
Jul 7, 2021, 3:50:09 PM7/7/21
to Blockly
We create the ID in the `mutationToDom` method if one doesn't exist (simplified):

```
mutationToDom(this) {
  const container = document.createElement("mutation");
  if !this._uuid {
    this._uuid = uuidV4();
  }
  container.setAttribute("uuid", this._uuid);
  return container;
}
```

And in `domToMutation` we do:
```
domToMutation(xmlElement) {
  this._uuid = xmlElement.getAttribute("uuid");
}
```

So when serializing, we create a UUID if the block doesn't have one already. When deserializing, we read in the UUID that was created before. When saving one of these blocks for the first time, we also make a secondary call to create an entity with the same UUID to track them together. This is why we were looking for a way to wipe the UUID on copy so that this logic would keep working. Here I think we would need to know whether we are deserializing copied code or not to try and handle it here.

Best,
Eric

Coda Highland

unread,
Jul 7, 2021, 4:45:29 PM7/7/21
to blo...@googlegroups.com
Did you know blocks already have UUIDs? Pasted blocks don't carry over the original block's auto-generated UUID. Perhaps you might not need to do this at all, then; I don't know your specific use case.

If you still need to have specific behaviors attached to it, you could perhaps copy the blockId into the mutation DOM. If they mismatch, then you know it's a copy-paste. If they match, you know you're loading existing data. If the mutation DOM doesn't have a UUID set, you know it's a newly-created block.

The one wrinkle that occurs to me is that you may wish to handle cut-and-paste differently from copy-and-paste if the user is intending to move blocks around. Again I don't know your use case, so this may not be a problem for you, or it may be an acceptable cost.

/s/ Adam

Agustin Sevilla

unread,
Jul 8, 2021, 2:20:32 AM7/8/21
to Blockly
Hello all! 

First off, thank you! I have been a long time lurker on here - first time posting. 

So we were able to solve this problem (Eric and I work together) largely thanks to your advice so far! Here is how we did it.

I really liked Adam's idea about using events, so I investigated that and ended up creating an Extension to listen for Create events by adding a `setOnChange` handler.

One thing I ran into was that I could not figure out how to change a mutation value from that extension, which actually led me to something great. While re-reading the docs I saw that our use case is better suited for the Block.data property, since it's literally "Optional text data that round-trips between blocks and XML. Has no effect. May be used by 3rd parties for meta information." Aha!  I moved the management of our system UUID over to data instead of using a mutator.

With that knowledge, we had the bulk of a solution! A way to listen to block create events and conditionally regenerate an ID. The problem then became that Blockly was obediently regenerating them on any change that triggered the create event - notably when re-opening a workspace (undesirable for us). I addressed this by cached the block ID as well as our system ID so we can check that before regenerating a new UUID. 

Here is a pseudocode showing how that works (we use typescript - hopefully the markdown here comes out right): 

```typescript
Blockly.Extensions.register(
    "system_id_manager",
    function (this: Blockly.Block) {
        this.setOnChange((changeEvent: Blockly.Events.Abstract) => {
            // Question: Is there a better way to get the event type than toJson? 
            if ((changeEvent.toJson() as { type: string }).type === "create") {
                if (!this.data || JSON.parse(this.data).block_id !== this.id) {
                    this.data = JSON.stringify({
                        system_id: generateSystemId(),
                        block_id: this.id,
                    });
                }
            }
        });
    }
);
```

From there, the rest was fairly straightforward! 
1) Update the original Mutator extension to migrate the IDs to block.data for older blocks 
2) We have a custom copy/paste context menu, and there I added some massaging of the selected block's DocumentFragment before adding it to the clipboard, stripping off the block ID and the `<data>` node to ensure we get new values. 

Question: I added a comment on the example code above where I had to serialize the ChangeEvent to access it's type. Is there a better way to get the event type that I a missing? I do not see it exposed on the BlockCreate or on the Events Abstract 🤔 
 
Many thanks again to everyone who chipped in! Your time and help is appreciated! 

Let me know if you have any suggestions (or questions)!

Cheers!

Augie 

PS: To answer Adam's question regarding us using the UUIDs from Blockly: While that would be awesome, unfortunately our API will not accept them as they do not comply with our spec for IDs. Too bad though! That would have been perfect, I think we could have gotten away with simply removing the IDs in the custom Copy to clipboard context menu action. 

Beka Westberg

unread,
Jul 8, 2021, 10:06:35 AM7/8/21
to blo...@googlegroups.com
Hi Augie,

> First off, thank you! I have been a long time lurker on here - first time posting.
Welcome :D

> So we were able to solve this problem...
That's awesome! I'm so glad you found something that works for you :D And thank you for sharing the code you ended up using! It is always nice to come back to an old forum post and see the complete solution.

> I added a comment on the example code above where I had to serialize the ChangeEvent to access it's type. Is there a better way to get the event type that I am missing? I do not see it exposed on the BlockCreate or on the Events Abstract 🤔 
Huh that's really weird, because the Events Abstract definitely has a type property. I think that the JSDoc -> reference docs generator is having some issues at the moment, so that could be what's happening. The documentation on the events guide page is hand-written so it should be more accurate. If you do stuff with events in the future I would check there :D

Thank you again for posting your solution!
--Beka

Reply all
Reply to author
Forward
0 new messages