Skip to content

fix: Make workspace-multiselect plugin workable with Blockly v12 #9261

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from

Conversation

HollowMan6
Copy link
Contributor

The basics

The details

Resolves

Fixes mit-cml/workspace-multiselect#103

Proposed Changes

Workspace-multiselect plugin is in the process of updating to Blockly v12:

However, some changes are necessary to get Blockly working properly with the plugin. The fundamental issue here is that Blockly v12 is buggy at this stage to properly support a custom and dummy IDraggable (MultiselectDraggable for the plugin's case), as Blockly constantly wants to change the focus to the actual block via hardcoded getFocusManager().focusNode calls that are scattered all around the codebase. Setting MultiselectDraggable directly via Blockly.common.setSelected doesn't work as well, and we will need to call Blockly.getFocusManager().updateFocusedNode directly.

I'm sure the changes in this PR now will break some native Blockly features here and there. Feel free to modify on top of this PR directly, and I'll help test out to see if it still works with the plugin.

Reason for Changes

Necessary changes to make workspace-multiselect plugin workable with Blockly v12

Test Coverage

N/A

Documentation

N/A

Additional Information

https://groups.google.com/g/blockly/c/7RSn9cXAulA

Comment on lines +304 to +307
// Make sure the element is of Node type
if (!(element instanceof Node)) {
element = null;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT this check does nothing (and I'm surprised that TSC does not complain), because IFocusableNode's getFocusableElement method is typed as returning HTMLElement | SVGElement and both HTMLElement and SVGElement inherit from Node, so the only time the body of the if statement will be executed is if element is already null.

If some focusable node's .getFocusableElement() returns something which is not an instanceof Node then it has violated the interface definition—unless I have greatly misunderstood something about how the DOM works, at any rate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If some focusable node's .getFocusableElement() returns something which is not an instanceof Node then it has violated the interface definition

For now the MultiselectDraggable is dummy and doesn't have an actual corresponding DOM object, that's why this check is added here, not sure if there's a better way for handling this one.

mit-cml/workspace-multiselect@1c9b6d5#diff-a6ef3f5605a5577d348743aa99d160ec6523f07f18c754f34e638513878515a5R47-R49

Comment on lines +877 to +880
// Make sure the element is of Node type
if (!(element instanceof Node)) {
element = null;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto here.

Comment on lines +331 to +333
// const prevFocusedElement = this.focusedNode?.getFocusableElement();
// const hasDesyncedState = prevFocusedElement !== document.activeElement;
const hasDesyncedState = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The multiselect plugin will definitely need to be able to operate without causing focus desynchronisation.

Copy link
Contributor Author

@HollowMan6 HollowMan6 Jul 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As explained:

Currently, the workspace-multiselect plugin acts as an adapter. It maintains its own multiple selection set (state), which keeps track of currently selected blocks. At the Blockly side, this is implemented as if there's a global MultiselectDraggable (which implements IDraggable) for each workspace, and current design for the plugin is that this instance of MultiselectDraggable should always be selected (in a dummy way as it won't create any new DOM object) when we have multiple blocks selected, so that the plugin can receive corresponding actions and pass all the actions to the blocks in the multiple selection set (state). It was working well in previous versions, but now in v12, the current code for getFocusManager().focusNode seems to break this completely, as it keeps unselecting MultiselectDraggable.

This change is an attempt to address the MultiselectDraggable unselecting issue, as we don't want the actual focus to get synchronised when in multiselect mode, although I'm aware that this will cause issues with other Blockly features.

if (!(element instanceof Node)) {
element = null;
}
const restoreFocus = this.getSvgRoot().contains(element);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible that the existing check, reproduced here, is overly-broad: I note that restoreFocus will get set to true even if this merely contains focusedNode (rather than is focusedNode).

I also wonder whether the focus manipulation in appendChild, which this section of code is working around, could itself be either eliminated or made more adept, such that this workaround would not be needed at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, the MultiselectDraggable is dummy and doesn't have an actual corresponding DOM object. If this is properly addressed, I would assume this change will already be eliminated.

Comment on lines +110 to +112
// // Since moving the element to the drag layer will cause it to lose focus,
// // ensure it regains focus (to ensure proper highlights & sent events).
// getFocusManager().focusNode(elem);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not obvious to me why moving an element to the drag layer will cause it to lose focus. Maybe that is the problem that should be fixed, so this section of code is not needed.

Copy link
Contributor Author

@HollowMan6 HollowMan6 Jul 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's because that we should keep MultiselectDraggable selected when we have multiple blocks selected, and when we do the dragging, we drag multiple blocks one by one by calling the method here. This code will make the single exact block that is in the dragging process get selected, instead of keeping MultiselectDraggable selected.

Comment on lines +128 to +131
// // Since moving the element off the drag layer will cause it to lose focus,
// // ensure it regains focus (to ensure proper highlights & sent events).
// getFocusManager().focusNode(elem);
// }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto here.

if (classNames.every((name) => element.classList.contains(name))) {
if (
classNames.every(
(name) => !!element.classList && element.classList.contains(name),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MDN tells me that Element's .classList will always be a DOMTokenList, even if the element has no class attribute, so I cannot see why the nullish check might be needed here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar reason as for now, the MultiselectDraggable is dummy and doesn't have an actual corresponding DOM object.

@@ -90,7 +96,9 @@ export function addClass(element: Element, className: string): boolean {
* @param classNames A string of one or multiple class names for an element.
*/
export function removeClasses(element: Element, classNames: string) {
element.classList.remove(...classNames.split(' '));
if (element.classList) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto here.

if (classNames.every((name) => !element.classList.contains(name))) {
if (
classNames.every(
(name) => !element.classList || !element.classList.contains(name),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto here.

@cpcallen cpcallen requested a review from BenHenning July 29, 2025 18:57
@HollowMan6 HollowMan6 marked this pull request as draft July 29, 2025 22:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
PR: fix Fixes a bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

cannot be used normally in Blockly 12
4 participants