d3/d3-brush

Disable Shift key behavior?

Closed this issue · 27 comments

Is there a best-practices way to disable the shift key behavior?

In my app the brush is enabled only when the user holds shift. I.e. they hold shift and then drag to select nodes in a force layout. This means that event.shiftKey is always true during the brush drag. I understand that this is a feature of the brush, but if possible I would like to disable it for this use case.

I scanned through the code and saw that if I change this line to the following everything works fine:

shifting = false, // Disable `shifting` entirely

But I wasn't sure how I might hook into this functionality without modifying the source. Any suggestions would be much appreciated. Thanks!

Hmm. You can register a brush start event listener that removes the brush’s keydown and keyup event listeners, thereby preventing it from receiving these events:

brush.on("start.nokey", function() {
  d3.select(window).on("keydown.brush keyup.brush", null);
});

But, it’ll still see event.shiftKey, event.metaKey and event.altKey if they are pressed during mousedown or touchstart, so that won’t really disable the behavior completely.

I think the simplest thing would probably be a new feature that lets you disable the event.shiftKey and other key-driven behaviors. Possibly it should let you remap them to different modifier keys, or null to remove the functionality entirely.

Thanks for the quick reply. As you said the mousedown and touchstart events are still coming through and the event.shiftKey is true when that event fires.

A remapping solution would be great 👍 since these are still useful features to have, it's just a bit inflexible currently.

I'll likely fork the lib and remove this functionality in the meantime but the customizable solution would be great.

If anyone stumbles upon this issue and is interested in my solution I did end up forking the lib and removing shifting functionality. As the project developed I also ended up tweaking a few other things about the brush. Namely, I found myself wanting to hook into events which were being blocked by the brush so I ended up removing nopropagation and noevent in some places.

More customizability in the way the brush binds to keyboard/mouse events would be really cool, but forking the lib is a viable solution for anyone else who runs into similar issues.

these are still useful features to have, it's just a bit inflexible currently

totally agree...maybe it could be improved to a optional feature?

It's not working to me, also remove keydown.brush and keyup.brush event, but shifting's init value has been true, and always true when brush.

I just got hit by this as well - FWIW I am trying to develop a view which has zoom, pan and brush - but I only wanted the brush enabled when the "shift" key was pressed.

Grabbing Shift or Alt key behavior with no possibility to reconfigure it is unsuitable.
In my application, I'm using Brush behavior to select several geometrical entities, I'm using Ctrl key to activate Brush behavior over standard Zoom-Pan behavior. Successive Brush selects new set of entities. With e.g. Shift or Alt pressed, I would like to activate other logical operations between successive selections, e.g. by adding or subtracting them.
Built-in behavior of Shift and Alt are in my application of minor significance and I would like to have them removable / reconfigurable.

@iansinnott would you mind sharing your fork? The brush behavior is fantastic, but the fact that it takes over the shift and alt keys really makes it difficult to integrate with other functionality like @GordonSmith's example of zooming and brushing.

Trying to tie it to another keydown event is tricky because it looks like other keys repeatedly fire the 'keydown' event while held down. The shift and alt keydown events only fire once when the key is held down continuously.

@pkerpedjiev I would be happy to but it's currently an internal fork so it's not publicly accessible.

I am indeed using it for zoom/pan/shift+brush, so what is suggested in this thread is quite possible and didn't take too much work. It was mostly a matter of code removal. I believe I removed everything relating to shifting (source) as well as noevent and nopropagation. The shifting behavior is simply undesirable if you want to use shift to activate the brush, and the event related stuff allows DOM events to bubble up and be caught by your app code instead of the brush internal handlers.

It seems like the main thing blocking this issue is not how to implement it but what the desired API is, both in terms of functionality and name.

As context, there are four modes of manipulating the brush. These modes are not exposed in the public API (currently), but here are the internal names that are used:

  • MODE_DRAG - Translates the brush selection, maintaining its current size. This typically happens when you click and drag in the main area of an existing brush selection.

  • MODE_SPACE - Like MODE_DRAG, translates the brush selection, maintaining its current size. The only difference is that this is enabled temporarily by holding down the SPACE key when in another mode, such as MODE_HANDLE.

  • MODE_HANDLE - Resizes one or two edges of the brush selection, maintaining the position of the other edges. This typically happens when you click and drag on an edge (or a corner) of an existing brush selection, or when you click outside the existing brush selection and drag to create a new brush selection.

  • MODE_CENTER - Like MODE_HANDLE, except it resizes two or four edges of the brush selection, maintaining the current center of the brush selection. This mode is temporarily enabled from MODE_HANDLE by holding down the ALT key. Also note that the SPACE key takes priority, so holding down ALT and SPACE will use MODE_SPACE.

Separate from the modes, there is also a flag:

  • shifting - Allow the brush selection to change in only one dimension. The dimension is determined by the first pointer movement after SHIFT is depressed. For example, if you hold down SHIFT and move the pointer to the right, the y-position of the brush selection becomes locked at its current values until the SHIFT key is released.

Some possible options:

  1. The simplest API would be to disable all key modifiers. This would allow you to use MODE_DRAG and MODE_HANDLE, depending on where the initial click landed, but would not allow you to change modes during a brush gesture, and would effectively disable MODE_SPACE, MODE_CENTER, and the shifting flag.

  2. An alternative API might allow disabling of specific modes. If you disabled MODE_SPACE, for instance, it would not listen for the SPACE key, but it might still listen for the ALT key to enable MODE_CENTER. Likewise you could disable MODE_CENTER to avoid listening for ALT. On the other hand since the shifting flag is orthogonal to the modes, I don’t see how this could disable listening for SHIFT, which is the original motivation for this issue.

  3. Another alternative would be to continue to observe key modifiers, but disable key listeners. This would allow you to use a key modifier at the start of a gesture; for example, holding down SHIFT on brush start could still lock the selection position on one dimension, but there would be no keydown and keyup listener, so if the SHIFT key were released while brushing, it would not affect the current brush gesture. Thus the brush would not interfere with external event listeners that want to do something with these key events.

Is it possible that all key modifiers start turned off, and we can assign certain keys to the modifiers?

Ie: brush.setModifier("CENTER", alt_key);

(I love how it works now, but I need to use shift for other functions in my app, so I just had to comment it out from my d3.js)

I wouldn’t change the default behavior, as that would require a major version change.

However I think allowing the key bindings to be reconfigurable could work. We’d need to enumerate the behavioral modifiers, say symbols like this:

  • d3.brushFixSize - defaults to SPACE (32)
  • d3.brushFixCenter - defaults to ALT (18)
  • d3.brushFixSecondary - defaults to SHIFT (16)

It’s a little weird in that we need to know the mapping from key code to key modifier (i.e., keycode 18 means event.altKey). But it’s probably doable? I worry it’s more complex than just an all-on/all-off flag, of course.

And d3.brushFixSize(null) would disable that behavior?

Probably something like this:

brush.keyModifier(d3.brushFixSize, null)

But as I said, that’s just one proposal, and I don’t know if the added complexity of configurable modifiers is justified in comparison to just being able to turn them all off.

Could you also be able to use the metaKey? or to set no key if we want to disable a function?

Having the option to just turn them all off is one thing. Very beneficial for those apps, like mine, that have Ctrl, Alt, and Shift mapped to other things and just want basic brush behaviors. In my case, I want to use Shift to be an additive selection like you had in your Draggable Network II example for v3. So just being able to disable them would work in my scenario.

Remapping the keys to allow for the same behavior is definitely more beneficial for a larger audience I believe. Maybe creating a shortcut function that performs a blanket disable by remapping each to null would be the solution for both issues brush.disableKeyModifiers(). What @mbostock suggested above brush.keyModifier(d3.brushFixSize, null) I think is fine. As long as what brushFixSize, brushFixCenter, and brushFixSecondary are documented well so that everyone knows what they mean. I also think @Frazer's suggestion of just using 'CENTER', 'SIZE', or 'SECONDARY' are viable as well. I would expect almost all users of D3 who are trying to set keydown functions would understand the mapping between keycodes and key modifiers. So I don't necessarily see that as a blocker to moving forward with this.

I've been trying to think of a way you could tie it into either the brush.on() functions or some other type of event override similar to your example above.

brush.on("start.nokey", function() {
  d3.select(window).on("keydown.brush keyup.brush", null);
});

But I can't come up with anything that feels viable.

I'm personally fine with turning off all of these behaviors at once. But that is also possible if each are individually configurable. So I suppose it depends on how much extra effort it takes to implement the latter option to determine whether it's worth it.

@kpaxton If you're interested, I implemented a selectable, draggable, force directed network by forking d3-brush.js and turning off the default shift key behavior. Example is on bl.ocks.org.

Thansk @pkerpedjiev but I'm in a production situation and don't want to stray from supported packages. I was using your other one initially. http://bl.ocks.org/pkerpedjiev/0389e39fad95e1cf29ce

Any movement on this yet? I could really use this feature.

I got hit by this as well, will be there any update regarding this behavior?

Is anyone going to work on this at all? Looks like this library has been dormant for a long time. @mbostock ?

+1

I could really use this behavior, as well; I need to map the modifiers to match the keys used in another package that I don't have control over.

https://github.com/krisdages/d3-brush/tree/issue-20

@mbostock,
I implemented according to the spec above, with the addition of adding another behavior "constant" for the meta key behavior.

Wasn't sure what the form of d3.brushFixSize etc, should take; I went ahead and made them objects that hold the default key code, and allow it to be changed with, e,g. d3.brushFixSize.keyCode(18).

I tried to follow the general pattern where calling a method without the value to set returns the current value.

Was not sure about code style or pull request etiquette for this project, so I didn't create a pull request, but please feel free to make any changes to this and pull them in if you want.

BTW, I noticed that event.keyCode is marked as deprecated on MDN. My initial plan was to recognize the modifier keys with event.code, event.char, and/or event.key instead of switch (event.keyCode) {}, but I didn't want to make too many changes.

My initial implementation used event.key alone, but it was problematic on my Linux system because
Shift + Alt emitted "Meta".

I’ve added brush.keyModifiers(false) as a quick way to turn off all keybindings. If we want something fancier, it’ll require further design.

After reading this thread and seeing examples like this one, I did the following to disable panning/zooming when the shift key is held...

zoom.filter(() => !d3.event.shiftKey)

and then used the opposite filter as well as the new keyModifiers method on the brush...

brush.filter(() => d3.event.shiftKey)
brush.keyModifiers(false)

This approach worked perfectly! Thanks @iansinnott for the initial thought/research and thanks @mbostock for the keyModifiers addition 🎉. For anyone else that stumbles upon this, you may want to add a few more cases to brush.filter to improve the experience or just hide the brush after selection is complete (as done here).