umbau 1.2
This commit is contained in:
@ -16,17 +16,24 @@
|
||||
* Copy & Paste: xterm.js liefert Markieren-zum-Kopieren + Strg+Umschalt+V
|
||||
* bereits nativ. Aufgabe #11 (vom Nutzer nachtraeglich hinzugefuegt, siehe
|
||||
* FORTSETZUNG_Aufgabe11.md) ergaenzt zwei konkret gewuenschte Wege:
|
||||
* `copyOnSelect: true` kopiert eine Markierung sofort in die
|
||||
* System-Zwischenablage (kein Strg+C/Cmd+C mehr noetig -- Strg+C bleibt
|
||||
* ohne Markierung weiterhin SIGINT, xterm.js' Standardverhalten). Rechtsklick
|
||||
* fuegt ein: zuerst wird direkt versucht, ueber die Clipboard-API einzufuegen
|
||||
* (funktioniert in den meisten Chromium-Browsern, da ein Rechtsklick eine
|
||||
* "transiente Aktivierung" ist); schlaegt das fehl (Firefox/Safari ohne
|
||||
* Berechtigung, unsicherer Kontext), erscheint ersatzweise ein winziges
|
||||
* Kontextmenue mit einem "Einfuegen"-Eintrag -- der Klick darauf ist selbst
|
||||
* eine Benutzergeste und hat dadurch nochmal bessere Chancen auf die
|
||||
* Berechtigung (siehe _wireClipboard() unten). Keine serverseitige
|
||||
* Sonderbehandlung noetig, im Gegensatz zu RDP (siehe rdp.js).
|
||||
* `copyOnSelect: true` versucht, eine Markierung sofort in die System-
|
||||
* Zwischenablage zu kopieren (kein Strg+C/Cmd+C mehr noetig -- Strg+C bleibt
|
||||
* ohne Markierung weiterhin SIGINT, xterm.js' Standardverhalten). Das ist
|
||||
* aber eine Automatik OHNE direkte Benutzergeste (sie laeuft aus xterm.js'
|
||||
* eigenem mouseup-Handler heraus) und schlaegt daher in der Praxis manchmal
|
||||
* fehl, ohne dass die App das mitbekommt (Live-Test 2.9.: Markierung+Ziehen
|
||||
* hat sichtbar funktioniert, in der Zwischenablage kam aber nichts an).
|
||||
* Deshalb zusaetzlich ein Rechtsklick-Kontextmenue, das PuTTYs bekannte
|
||||
* Konvention nachbildet: Rechtsklick MIT aktiver Markierung kopiert (direkter
|
||||
* Versuch ueber die Clipboard-API, da ein Rechtsklick selbst eine
|
||||
* "transiente Aktivierung"/Benutzergeste ist -- das ist deutlich
|
||||
* zuverlaessiger als copyOnSelects impliziter Aufruf); schlaegt das fehl
|
||||
* oder ist keine Markierung aktiv, faellt es auf "Einfuegen" zurueck --
|
||||
* ebenfalls zuerst direkt ueber die Clipboard-API versucht, sonst ein
|
||||
* winziges Kontextmenue mit "Kopieren"- bzw. "Einfuegen"-Eintrag, dessen
|
||||
* Klick selbst wieder eine frische Benutzergeste ist (siehe _wireClipboard()
|
||||
* unten). Keine serverseitige Sonderbehandlung noetig, im Gegensatz zu RDP
|
||||
* (siehe rdp.js).
|
||||
* Markieren mit gedrueckter Umschalttaste: xterm.js reicht Mausklicks an ein
|
||||
* Vollbild-Zielprogramm mit eigener Maussteuerung (tmux, vim, htop, mc, ...)
|
||||
* weiter, sobald dieses per ANSI-Escape-Sequenz Maus-Tracking anfordert --
|
||||
@ -187,7 +194,7 @@
|
||||
this._lastErrorMessage = null;
|
||||
this._transfers = [];
|
||||
this._resizeDebounce = null;
|
||||
this._pasteMenuEl = null;
|
||||
this._clipboardMenuEl = null;
|
||||
|
||||
this.element = this._buildElement();
|
||||
this._cacheRefs();
|
||||
@ -442,7 +449,7 @@
|
||||
this.sendResize();
|
||||
this.term.focus();
|
||||
} else {
|
||||
this._closePasteMenu();
|
||||
this._closeClipboardMenu();
|
||||
}
|
||||
}
|
||||
|
||||
@ -475,21 +482,42 @@
|
||||
});
|
||||
}
|
||||
|
||||
// --- Zwischenablage (Aufgabe #11) --------------------------------------
|
||||
// --- Zwischenablage (Aufgabe #11, erweitert Live-Test 2.9.) ------------
|
||||
//
|
||||
// Kopieren laeuft ueber copyOnSelect (siehe _setupTerminal()) -- hier nur
|
||||
// Einfuegen per Rechtsklick. Zwei Stufen, wie im Datei-Kopfkommentar
|
||||
// beschrieben: direkter Versuch, dann ein winziges Kontextmenue als
|
||||
// Ruecksicherung, wenn der Browser die Berechtigung ohne explizite
|
||||
// Benutzergeste verweigert.
|
||||
// copyOnSelect (siehe _setupTerminal()) bleibt als Automatik aktiv, ist
|
||||
// aber nicht zuverlaessig genug (siehe Datei-Kopfkommentar) -- deshalb
|
||||
// zusaetzlich Rechtsklick nach PuTTY-Konvention: MIT aktiver Markierung
|
||||
// kopieren, OHNE einfuegen. Beides zweistufig wie gehabt: direkter
|
||||
// Clipboard-API-Versuch, bei Fehlschlag ein winziges Kontextmenue als
|
||||
// Ruecksicherung (dessen Klick selbst wieder eine frische Benutzergeste
|
||||
// ist und dadurch bessere Chancen auf die Berechtigung hat).
|
||||
|
||||
_wireClipboard() {
|
||||
this.terminalContainer.addEventListener("contextmenu", (ev) => {
|
||||
ev.preventDefault();
|
||||
this._attemptDirectPaste(ev.clientX, ev.clientY);
|
||||
if (this.term.hasSelection()) {
|
||||
this._attemptDirectCopy(ev.clientX, ev.clientY);
|
||||
} else {
|
||||
this._attemptDirectPaste(ev.clientX, ev.clientY);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async _attemptDirectCopy(x, y) {
|
||||
const text = this.term.getSelection();
|
||||
if (!navigator.clipboard || !navigator.clipboard.writeText) {
|
||||
this._showCopyMenu(x, y, text);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch (_) {
|
||||
// Wie bei _attemptDirectPaste(): Ruecksicherung unten, deren Klick
|
||||
// selbst wieder eine frische Benutzergeste ist.
|
||||
this._showCopyMenu(x, y, text);
|
||||
}
|
||||
}
|
||||
|
||||
async _attemptDirectPaste(x, y) {
|
||||
if (!navigator.clipboard || !navigator.clipboard.readText) {
|
||||
this._showPasteMenu(x, y);
|
||||
@ -506,19 +534,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
_closePasteMenu() {
|
||||
if (this._pasteMenuEl) {
|
||||
this._pasteMenuEl.remove();
|
||||
this._pasteMenuEl = null;
|
||||
_closeClipboardMenu() {
|
||||
if (this._clipboardMenuEl) {
|
||||
this._clipboardMenuEl.remove();
|
||||
this._clipboardMenuEl = null;
|
||||
}
|
||||
if (this._pasteMenuCleanup) {
|
||||
this._pasteMenuCleanup();
|
||||
this._pasteMenuCleanup = null;
|
||||
if (this._clipboardMenuCleanup) {
|
||||
this._clipboardMenuCleanup();
|
||||
this._clipboardMenuCleanup = null;
|
||||
}
|
||||
}
|
||||
|
||||
_showPasteMenu(x, y) {
|
||||
this._closePasteMenu();
|
||||
// Gemeinsamer Aufbau fuer Kopieren- und Einfuegen-Mini-Kontextmenue --
|
||||
// beide brauchen dieselbe Positionierung und dasselbe Schliessverhalten,
|
||||
// nur Beschriftung und Klick-Aktion unterscheiden sich.
|
||||
_showClipboardMenu(x, y, label, onButtonClick) {
|
||||
this._closeClipboardMenu();
|
||||
// CSP (style-src 'self', kein 'unsafe-inline', siehe app.css-
|
||||
// Kopfkommentar zu .hidden) verbietet JS-gesetzte element.style-
|
||||
// Aenderungen genauso wie style="..."-Attribute -- daher KEINE
|
||||
@ -534,11 +565,50 @@
|
||||
(isBottom ? "term-paste-menu-bottom" : "term-paste-menu-top") + " " +
|
||||
(isRight ? "term-paste-menu-right" : "term-paste-menu-left");
|
||||
|
||||
const pasteBtn = document.createElement("button");
|
||||
pasteBtn.type = "button";
|
||||
pasteBtn.textContent = "Einfuegen";
|
||||
pasteBtn.addEventListener("click", async () => {
|
||||
this._closePasteMenu();
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.textContent = label;
|
||||
btn.addEventListener("click", async () => {
|
||||
this._closeClipboardMenu();
|
||||
await onButtonClick();
|
||||
this.term.focus();
|
||||
});
|
||||
menu.appendChild(btn);
|
||||
|
||||
// Als Kind von .session-shell (nicht .session-terminal) angehaengt --
|
||||
// dort mischt sich nichts mit xterm.js' eigenem DOM-Teilbaum, das
|
||||
// dieses Element seit term.open() exklusiv verwaltet.
|
||||
this.shellEl.appendChild(menu);
|
||||
this._clipboardMenuEl = menu;
|
||||
|
||||
// Schliessen bei Klick daneben, Escape oder Scrollen -- Standard-
|
||||
// verhalten fuer ein solches Mini-Kontextmenue (siehe auch
|
||||
// workspace.js fuer dasselbe Muster bei anderen Overlays).
|
||||
const onDocClick = (ev) => {
|
||||
if (!menu.contains(ev.target)) this._closeClipboardMenu();
|
||||
};
|
||||
const onKeydown = (ev) => {
|
||||
if (ev.key === "Escape") this._closeClipboardMenu();
|
||||
};
|
||||
const onScroll = () => this._closeClipboardMenu();
|
||||
// capture:true fuer den Klick-Listener, damit er VOR einem moeglichen
|
||||
// contextmenu-Listener eines anderen Elements greift; im naechsten
|
||||
// Tick registriert, sonst schliesst der Rechtsklick, der das Menue
|
||||
// gerade erst geoeffnet hat, es sofort wieder (derselbe Event).
|
||||
window.setTimeout(() => {
|
||||
document.addEventListener("click", onDocClick, true);
|
||||
document.addEventListener("keydown", onKeydown);
|
||||
window.addEventListener("scroll", onScroll, true);
|
||||
}, 0);
|
||||
this._clipboardMenuCleanup = () => {
|
||||
document.removeEventListener("click", onDocClick, true);
|
||||
document.removeEventListener("keydown", onKeydown);
|
||||
window.removeEventListener("scroll", onScroll, true);
|
||||
};
|
||||
}
|
||||
|
||||
_showPasteMenu(x, y) {
|
||||
this._showClipboardMenu(x, y, "Einfuegen", async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
this.term.paste(text);
|
||||
@ -549,40 +619,21 @@
|
||||
true
|
||||
);
|
||||
}
|
||||
this.term.focus();
|
||||
});
|
||||
menu.appendChild(pasteBtn);
|
||||
}
|
||||
|
||||
// Als Kind von .session-shell (nicht .session-terminal) angehaengt --
|
||||
// dort mischt sich nichts mit xterm.js' eigenem DOM-Teilbaum, das
|
||||
// dieses Element seit term.open() exklusiv verwaltet.
|
||||
this.shellEl.appendChild(menu);
|
||||
this._pasteMenuEl = menu;
|
||||
|
||||
// Schliessen bei Klick daneben, Escape oder Scrollen -- Standard-
|
||||
// verhalten fuer ein solches Mini-Kontextmenue (siehe auch
|
||||
// workspace.js fuer dasselbe Muster bei anderen Overlays).
|
||||
const onDocClick = (ev) => {
|
||||
if (!menu.contains(ev.target)) this._closePasteMenu();
|
||||
};
|
||||
const onKeydown = (ev) => {
|
||||
if (ev.key === "Escape") this._closePasteMenu();
|
||||
};
|
||||
const onScroll = () => this._closePasteMenu();
|
||||
// capture:true fuer den Klick-Listener, damit er VOR einem moeglichen
|
||||
// contextmenu-Listener eines anderen Elements greift; im naechsten
|
||||
// Tick registriert, sonst schliesst der Rechtsklick, der das Menue
|
||||
// gerade erst geoeffnet hat, es sofort wieder (derselbe Event).
|
||||
window.setTimeout(() => {
|
||||
document.addEventListener("click", onDocClick, true);
|
||||
document.addEventListener("keydown", onKeydown);
|
||||
window.addEventListener("scroll", onScroll, true);
|
||||
}, 0);
|
||||
this._pasteMenuCleanup = () => {
|
||||
document.removeEventListener("click", onDocClick, true);
|
||||
document.removeEventListener("keydown", onKeydown);
|
||||
window.removeEventListener("scroll", onScroll, true);
|
||||
};
|
||||
_showCopyMenu(x, y, text) {
|
||||
this._showClipboardMenu(x, y, "Kopieren", async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch (err) {
|
||||
console.error("Kopieren in die Zwischenablage nicht moeglich:", err);
|
||||
this._setStatus(
|
||||
"Kopieren nicht moeglich -- Browser verweigert Zwischenablage-Zugriff. Bitte Strg+Umschalt+C verwenden.",
|
||||
true
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- Dateitransfer-Panel ---------------------------------------------
|
||||
@ -752,7 +803,7 @@
|
||||
window.clearInterval(this._pingTimer);
|
||||
this._pingTimer = null;
|
||||
}
|
||||
this._closePasteMenu();
|
||||
this._closeClipboardMenu();
|
||||
try {
|
||||
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
|
||||
this.ws.close(1000, "Sitzung durch Benutzer beendet");
|
||||
|
||||
Reference in New Issue
Block a user