mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
A key held from the menu (Hold Ctrl, Alt or Windows) is the same key as the keyboard's left Ctrl, Alt or Windows key. Pressing and releasing that key on the screen let the VM's key go, while the menu still showed it held. And when the key went down outside the screen (in the Clipboard dialog) and came up on it, noVNC sent no release, as it had not seen the key go down. The release of that key where noVNC takes keys (the screen, the touch keyboard) now lets a held key go itself, and clears the menu's mark. A release elsewhere reaches no VM and changes nothing.
112 lines
4.6 KiB
JavaScript
112 lines
4.6 KiB
JavaScript
// The keys menu of the VNC console page: key combinations the browser or the
|
|
// operating system would otherwise catch, and modifiers a device lacks.
|
|
import KeyTable from './novnc/core/input/keysym.js';
|
|
|
|
const CTRL = { keysym: KeyTable.XK_Control_L, code: 'ControlLeft' };
|
|
const ALT = { keysym: KeyTable.XK_Alt_L, code: 'AltLeft' };
|
|
const SUPER = { keysym: KeyTable.XK_Super_L, code: 'MetaLeft' };
|
|
const HOLDS = [['holdctrl', CTRL], ['holdalt', ALT], ['holdsuper', SUPER]];
|
|
|
|
function hold(rfb, id, key, down) {
|
|
const item = document.getElementById(id);
|
|
rfb.sendKey(key.keysym, key.code, down);
|
|
item.setAttribute('aria-pressed', down);
|
|
item.classList.toggle('active', down);
|
|
}
|
|
|
|
// the held modifiers up: before a combination from the menu (which presses
|
|
// and releases its own), and before the page stops sending keys
|
|
export function releaseHeld(rfb) {
|
|
for (const [id, key] of HOLDS) {
|
|
if (document.getElementById(id).getAttribute('aria-pressed') === 'true') {
|
|
hold(rfb, id, key, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
// press the keys in order, release them in reverse
|
|
function sendKeys(rfb, keys) {
|
|
releaseHeld(rfb);
|
|
keys.forEach((key) => rfb.sendKey(key.keysym, key.code, true));
|
|
setTimeout(() => [...keys].reverse().forEach((key) => rfb.sendKey(key.keysym, key.code, false)), 20);
|
|
}
|
|
|
|
export function bindKeyAction(id, handler) {
|
|
const el = document.getElementById(id);
|
|
if (el) {
|
|
el.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
handler();
|
|
});
|
|
}
|
|
}
|
|
|
|
export function bindSendKeys(rfb) {
|
|
const send = (...keys) => () => sendKeys(rfb, keys);
|
|
bindKeyAction('ctrlaltdel', () => {
|
|
releaseHeld(rfb);
|
|
rfb.sendCtrlAltDel();
|
|
});
|
|
bindKeyAction('ctrlaltbackspace', send(CTRL, ALT, { keysym: KeyTable.XK_BackSpace, code: 'Backspace' }));
|
|
bindKeyAction('alttab', send(ALT, { keysym: KeyTable.XK_Tab, code: 'Tab' }));
|
|
bindKeyAction('superkey', send(SUPER));
|
|
bindKeyAction('sendesc', send({ keysym: KeyTable.XK_Escape, code: 'Escape' }));
|
|
bindKeyAction('sendtab', send({ keysym: KeyTable.XK_Tab, code: 'Tab' }));
|
|
bindKeyAction('ctrlc', send(CTRL, { keysym: KeyTable.XK_c, code: 'KeyC' }));
|
|
bindKeyAction('ctrlz', send(CTRL, { keysym: KeyTable.XK_z, code: 'KeyZ' }));
|
|
bindKeyAction('ctrld', send(CTRL, { keysym: KeyTable.XK_d, code: 'KeyD' }));
|
|
bindKeyAction('ctrlbackslash', send(CTRL, { keysym: KeyTable.XK_backslash, code: 'Backslash' }));
|
|
for (let i = 1; i <= 12; i++) {
|
|
bindKeyAction('ctrlaltf' + i, send(CTRL, ALT, { keysym: KeyTable['XK_F' + i], code: 'F' + i }));
|
|
}
|
|
// a held modifier stays down until it is chosen again: with the next key
|
|
// typed it makes a combination the menu does not list
|
|
for (const [id, key] of HOLDS) {
|
|
bindKeyAction(id, () => {
|
|
// a view-only page sends no keys: nothing to hold
|
|
if (!rfb.viewOnly) {
|
|
hold(rfb, id, key, document.getElementById(id).getAttribute('aria-pressed') !== 'true');
|
|
}
|
|
});
|
|
}
|
|
// the same key on the keyboard, where noVNC takes keys (the screen, the
|
|
// touch keyboard): its release lets the held key go. Sent here, seen
|
|
// before noVNC (which keeps the event to itself): noVNC sends no release
|
|
// of a key it did not see go down, as when it went down in a dialog.
|
|
for (const target of [document.getElementById('screen'), document.getElementById('keyboard_input')]) {
|
|
target.addEventListener('keyup', (e) => {
|
|
for (const [id, key] of HOLDS) {
|
|
if (e.code === key.code && document.getElementById(id).getAttribute('aria-pressed') === 'true') {
|
|
hold(rfb, id, key, false);
|
|
}
|
|
}
|
|
}, true);
|
|
}
|
|
}
|
|
|
|
// the Fullscreen item; in full screen the page also takes the keys the
|
|
// browser would keep (Alt, Tab, Escape) where the browser lets it
|
|
export function bindFullscreen() {
|
|
const item = document.getElementById('fullscreen_button');
|
|
if (!document.fullscreenEnabled) {
|
|
item.hidden = true;
|
|
return;
|
|
}
|
|
bindKeyAction('fullscreen_button', () => {
|
|
if (document.fullscreenElement) {
|
|
document.exitFullscreen();
|
|
} else {
|
|
document.documentElement.requestFullscreen().catch(() => {});
|
|
}
|
|
});
|
|
document.addEventListener('fullscreenchange', () => {
|
|
if (!navigator.keyboard) {
|
|
return;
|
|
}
|
|
if (document.fullscreenElement) {
|
|
navigator.keyboard.lock(['AltLeft', 'AltRight', 'Tab', 'Escape']).catch(() => {});
|
|
} else {
|
|
navigator.keyboard.unlock();
|
|
}
|
|
});
|
|
}
|