Open ahuang11 opened 2 years ago
Something like this?
import panel as pn
script = """
<script>
const doc = window.parent.document;
buttons = Array.from(doc.querySelectorAll('button[type=button]'));
const left_button = buttons.find(el => el.innerText === 'LEFT');
const right_button = buttons.find(el => el.innerText === 'RIGHT');
doc.addEventListener('keydown', function(e) {
switch (e.keyCode) {
case 37: // (37 = left arrow)
left_button.click();
break;
case 39: // (39 = right arrow)
right_button.click();
break;
}
});
</script>
"""
html = pn.pane.HTML(script)
button_left = pn.widgets.Button(name="LEFT")
button_right = pn.widgets.Button(name="RIGHT")
button_left.on_click(lambda x: print("left"))
# OR
pn.bind(lambda: print("right"), button_right, watch=True)
pn.Row(
pn.Column(button_left, button_left.param.clicks),
pn.Column(button_right, button_right.param.clicks),
html,
).servable()
Neat! I wonder if it's possible to have a built-in "hotkey=..." keyword for each widget
I suppose this doesn't work anymore with intro of shadow root (or at least much more convoluted).
switch (e.keyCode) {
case 37: // (37 = left arrow)
keyCode
is a legacy API, use key
instead. See here for a list of possible key names, or see bokehjs' type definitions (note it's incomplete; only what we actually use).
An updated example that works with Panel 1. I have updated doc.querySelectorAll
to be able to pierce through the shadow dom with the $$$
function and updates to use the key
API.
import panel as pn
script = """
<script>
function $$$(selector, rootNode=document.body) {
const arr = []
const traverser = node => {
// 1. decline all nodes that are not elements
if(node.nodeType !== Node.ELEMENT_NODE) {
return
}
// 2. add the node to the array, if it matches the selector
if(node.matches(selector)) {
arr.push(node)
}
// 3. loop through the children
const children = node.children
if (children.length) {
for(const child of children) {
traverser(child)
}
}
// 4. check for shadow DOM, and loop through it's children
const shadowRoot = node.shadowRoot
if (shadowRoot) {
const shadowChildren = shadowRoot.children
for(const shadowChild of shadowChildren) {
traverser(shadowChild)
}
}
}
traverser(rootNode)
return arr
}
const doc = window.parent.document;
buttons = Array.from($$$('button[type=button]'));
const left_button = buttons.find(el => el.innerText === 'LEFT');
const right_button = buttons.find(el => el.innerText === 'RIGHT');
doc.addEventListener('keydown', function(e) {
switch (e.key) {
case "ArrowLeft":
left_button.click();
break;
case "ArrowRight":
right_button.click();
break;
}
});
</script>
"""
html = pn.pane.HTML(script)
button_left = pn.widgets.Button(name="LEFT")
button_right = pn.widgets.Button(name="RIGHT")
button_left.on_click(lambda x: print("left"))
button_right.on_click(lambda x: print("right"))
pn.Row(
pn.Column(button_left, button_left.param.clicks),
pn.Column(button_right, button_right.param.clicks),
html,
).servable()
@Hoxbro thanks a lot for the example!
Could you please tell if there is a way to remove this event listener while running in Jupyter Lab when the kernel is stopped or restarted?
Now that Panel 1.5 is out, I tried using ReactComponent to implement global keyboard shortcuts in a generalized way. This will also deregister the keyboard shortcuts if the KeyboardShortcuts component is unrendered.
from typing import TypedDict, NotRequired
# Note: this uses TypedDict instead of Pydantic or dataclass because Bokeh/Panel doesn't seem to
# like serializing custom classes to the frontend (and I can't figure out how to customize that).
class KeyboardShortcut(TypedDict):
name: str
key: str
altKey: NotRequired[bool]
ctrlKey: NotRequired[bool]
metaKey: NotRequired[bool]
shiftKey: NotRequired[bool]
class KeyboardShortcuts(ReactComponent):
"""
Class to install global keyboard shortcuts into a Panel app.
Pass in shortcuts as a list of KeyboardShortcut dictionaries, and then handle shortcut events in Python
by calling `on_msg` on this component. The `name` field of the matching KeyboardShortcut will be sent as the `data`
field in the `DataEvent`.
Example:
>>> shortcuts = [
KeyboardShortcut(name="save", key="s", ctrlKey=True),
KeyboardShortcut(name="print", key="p", ctrlKey=True),
]
>>> shortcuts_component = KeyboardShortcuts(shortcuts=shortcuts)
>>> def handle_shortcut(event: DataEvent):
if event.data == "save":
print("Save shortcut pressed!")
elif event.data == "print":
print("Print shortcut pressed!")
>>> shortcuts_component.on_msg(handle_shortcut)
"""
shortcuts = param.List(class_=dict)
_esm = """
// Hash a shortcut into a string for use in a dictionary key (booleans / null / undefined are coerced into 1 or 0)
function hashShortcut({ key, altKey, ctrlKey, metaKey, shiftKey }) {
return `${key}.${+!!altKey}.${+!!ctrlKey}.${+!!metaKey}.${+!!shiftKey}`;
}
export function render({ model }) {
const [shortcuts] = model.useState("shortcuts");
const keyedShortcuts = {};
for (const shortcut of shortcuts) {
keyedShortcuts[hashShortcut(shortcut)] = shortcut.name;
}
function onKeyDown(e) {
const name = keyedShortcuts[hashShortcut(e)];
if (name) {
e.preventDefault();
e.stopPropagation();
model.send_msg(name);
return;
}
}
React.useEffect(() => {
window.addEventListener('keydown', onKeyDown);
return () => {
window.removeEventListener('keydown', onKeyDown);
};
});
return <></>;
}
"""
https://gist.github.com/brunomsantiago/e0ab366fc0dbc092c1a5946b30caa9a0 Like that