115 lines
4.0 KiB
JavaScript
115 lines
4.0 KiB
JavaScript
/**
|
|
* Host → sandbox paste for the chrome-less noVNC embed.
|
|
*
|
|
* Debian noVNC 1.3 only syncs clipboard from the full vnc.html panel.
|
|
* Without this bridge, Ctrl/Cmd+V only forwarded keys: on macOS Cmd+V became
|
|
* Super+V (no paste on Linux), and the remote CLIPBOARD stayed empty.
|
|
*/
|
|
|
|
export const KEYSYM = {
|
|
Control_L: 0xffe3,
|
|
Control_R: 0xffe4,
|
|
Alt_L: 0xffe9,
|
|
Alt_R: 0xffea,
|
|
Super_L: 0xffeb,
|
|
Super_R: 0xffec,
|
|
Mode_switch: 0xff7e,
|
|
ISO_Level3_Shift: 0xfe03,
|
|
v: 0x76,
|
|
};
|
|
|
|
/** @param {{ ctrlKey?: boolean, metaKey?: boolean, altKey?: boolean, code?: string, key?: string }} event */
|
|
export function isPasteChord(event) {
|
|
if (!event || event.altKey) return false;
|
|
if (!(event.ctrlKey || event.metaKey)) return false;
|
|
return event.code === "KeyV" || event.key === "v" || event.key === "V";
|
|
}
|
|
|
|
/** @param {{ clipboardData?: DataTransfer | null }} event */
|
|
export function clipboardTextFromPaste(event) {
|
|
const data = event?.clipboardData;
|
|
if (!data) return "";
|
|
return data.getData("text/plain") || data.getData("text") || "";
|
|
}
|
|
|
|
/** True when noVNC RFB can accept clipboard and key events. */
|
|
export function isRfbConnected(rfb) {
|
|
const state = rfb?._rfbConnectionState;
|
|
return !state || state === "connected";
|
|
}
|
|
|
|
/**
|
|
* Release modifiers that may still be down from the host paste chord.
|
|
* noVNC 1.3 remaps macOS MetaLeft→Alt_L and MetaRight→Super_L (and Alt→
|
|
* Mode_switch / ISO_Level3_Shift), so release those keysyms too.
|
|
* @param {(keysym: number, code: string, down?: boolean) => void} sendKey
|
|
*/
|
|
export function releaseModifierKeys(sendKey) {
|
|
sendKey(KEYSYM.Control_L, "ControlLeft", false);
|
|
sendKey(KEYSYM.Control_R, "ControlRight", false);
|
|
sendKey(KEYSYM.Alt_L, "AltLeft", false);
|
|
sendKey(KEYSYM.Alt_R, "AltRight", false);
|
|
sendKey(KEYSYM.Super_L, "MetaLeft", false);
|
|
sendKey(KEYSYM.Super_R, "MetaRight", false);
|
|
sendKey(KEYSYM.Mode_switch, "AltLeft", false);
|
|
sendKey(KEYSYM.ISO_Level3_Shift, "AltRight", false);
|
|
}
|
|
|
|
/** Send Linux Ctrl+V so the focused remote app pastes from CLIPBOARD. */
|
|
export function sendRemotePaste(sendKey) {
|
|
sendKey(KEYSYM.Control_L, "ControlLeft", true);
|
|
sendKey(KEYSYM.v, "KeyV", true);
|
|
sendKey(KEYSYM.v, "KeyV", false);
|
|
sendKey(KEYSYM.Control_L, "ControlLeft", false);
|
|
}
|
|
|
|
/**
|
|
* Sync host paste into the RFB session, then paste into the remote desktop.
|
|
* @param {{ viewOnly?: boolean, clipboardPasteFrom?: (text: string) => void, sendKey?: Function, _rfbConnectionState?: string }} rfb
|
|
* @param {string} text
|
|
*/
|
|
export function pasteHostText(rfb, text) {
|
|
if (!rfb || rfb.viewOnly || !text || !isRfbConnected(rfb)) return false;
|
|
const sendKey = rfb.sendKey?.bind(rfb);
|
|
if (!sendKey || typeof rfb.clipboardPasteFrom !== "function") return false;
|
|
releaseModifierKeys(sendKey);
|
|
rfb.clipboardPasteFrom(text);
|
|
sendRemotePaste(sendKey);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param {{ viewOnly?: boolean, clipboardPasteFrom?: (text: string) => void, sendKey?: Function, _rfbConnectionState?: string }} rfb
|
|
* @param {{ target?: EventTarget }} [options]
|
|
* @returns {() => void} detach
|
|
*/
|
|
export function attachHostClipboardPaste(rfb, options = {}) {
|
|
const target = options.target ?? globalThis;
|
|
const onKeyDown = (event) => {
|
|
if (rfb.viewOnly || !isPasteChord(event)) return;
|
|
// Keep default so the browser still fires `paste` with clipboardData.
|
|
event.stopPropagation();
|
|
};
|
|
const onKeyUp = (event) => {
|
|
if (rfb.viewOnly || !isPasteChord(event)) return;
|
|
event.stopPropagation();
|
|
};
|
|
const onPaste = (event) => {
|
|
if (rfb.viewOnly) return;
|
|
const text = clipboardTextFromPaste(event);
|
|
if (!text) return;
|
|
if (!pasteHostText(rfb, text)) return;
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
};
|
|
|
|
target.addEventListener("keydown", onKeyDown, true);
|
|
target.addEventListener("keyup", onKeyUp, true);
|
|
target.addEventListener("paste", onPaste, true);
|
|
return () => {
|
|
target.removeEventListener("keydown", onKeyDown, true);
|
|
target.removeEventListener("keyup", onKeyUp, true);
|
|
target.removeEventListener("paste", onPaste, true);
|
|
};
|
|
}
|