feat: redesign mobile keyboard
This commit is contained in:
File diff suppressed because one or more lines are too long
+586
-155
@@ -752,7 +752,7 @@ type VirtualKeyboardKey = {
|
|||||||
value?: string;
|
value?: string;
|
||||||
seq?: string;
|
seq?: string;
|
||||||
width?: number;
|
width?: number;
|
||||||
actionId?: "mode-alpha" | "mode-symbol";
|
actionId?: VirtualKeyboardActionId;
|
||||||
};
|
};
|
||||||
|
|
||||||
const VIRTUAL_KEYBOARD_ALPHA_LAYOUT: VirtualKeyboardKey[][] = [
|
const VIRTUAL_KEYBOARD_ALPHA_LAYOUT: VirtualKeyboardKey[][] = [
|
||||||
@@ -780,7 +780,7 @@ const VIRTUAL_KEYBOARD_ALPHA_LAYOUT: VirtualKeyboardKey[][] = [
|
|||||||
{ kind: "char", label: "l", shiftLabel: "L" },
|
{ kind: "char", label: "l", shiftLabel: "L" },
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ kind: "modifier", label: "⇧", modifier: "shift", width: 1.35 },
|
{ kind: "modifier", label: "⇧", modifier: "shift" },
|
||||||
{ kind: "char", label: "z", shiftLabel: "Z" },
|
{ kind: "char", label: "z", shiftLabel: "Z" },
|
||||||
{ kind: "char", label: "x", shiftLabel: "X" },
|
{ kind: "char", label: "x", shiftLabel: "X" },
|
||||||
{ kind: "char", label: "c", shiftLabel: "C" },
|
{ kind: "char", label: "c", shiftLabel: "C" },
|
||||||
@@ -788,13 +788,13 @@ const VIRTUAL_KEYBOARD_ALPHA_LAYOUT: VirtualKeyboardKey[][] = [
|
|||||||
{ kind: "char", label: "b", shiftLabel: "B" },
|
{ kind: "char", label: "b", shiftLabel: "B" },
|
||||||
{ kind: "char", label: "n", shiftLabel: "N" },
|
{ kind: "char", label: "n", shiftLabel: "N" },
|
||||||
{ kind: "char", label: "m", shiftLabel: "M" },
|
{ kind: "char", label: "m", shiftLabel: "M" },
|
||||||
{ kind: "action", label: "⌫", seq: "\x7f", width: 1.35 },
|
{ kind: "action", label: "⌫", value: "Backspace", seq: "\x7f" },
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ kind: "action", label: "✂︎", seq: "\x1b", width: 1.1 },
|
{ kind: "action", label: "123", actionId: "mode-symbol" },
|
||||||
{ kind: "space", label: "␣", value: " ", width: 4 },
|
{ kind: "space", label: "␣", value: " ", width: 4 },
|
||||||
{ kind: "action", label: "⏎", seq: "\r", width: 1.35 },
|
{ kind: "action", label: "⏎", seq: "\r" },
|
||||||
{ kind: "action", label: "🌐", actionId: "mode-symbol", width: 1.1 },
|
{ kind: "action", label: "✂︎", actionId: "mode-selection" },
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -824,7 +824,7 @@ const VIRTUAL_KEYBOARD_SYMBOL_LAYOUT: VirtualKeyboardKey[][] = [
|
|||||||
{ kind: "char", label: "\"", value: "\"" },
|
{ kind: "char", label: "\"", value: "\"" },
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ kind: "action", label: "ABC", actionId: "mode-alpha", width: 1.35 },
|
{ kind: "action", label: "Esc", seq: "\x1b", width: 1.35 },
|
||||||
{ kind: "char", label: ".", value: "." },
|
{ kind: "char", label: ".", value: "." },
|
||||||
{ kind: "char", label: ",", value: "," },
|
{ kind: "char", label: ",", value: "," },
|
||||||
{ kind: "char", label: "?", value: "?" },
|
{ kind: "char", label: "?", value: "?" },
|
||||||
@@ -832,23 +832,64 @@ const VIRTUAL_KEYBOARD_SYMBOL_LAYOUT: VirtualKeyboardKey[][] = [
|
|||||||
{ kind: "char", label: "'", value: "'" },
|
{ kind: "char", label: "'", value: "'" },
|
||||||
{ kind: "char", label: "[", value: "[" },
|
{ kind: "char", label: "[", value: "[" },
|
||||||
{ kind: "char", label: "]", value: "]" },
|
{ kind: "char", label: "]", value: "]" },
|
||||||
{ kind: "action", label: "⌫", seq: "\x7f", width: 1.35 },
|
{ kind: "action", label: "⌫", value: "Backspace", seq: "\x7f", width: 1.35 },
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ kind: "action", label: "Esc", seq: "\x1b", width: 1.1 },
|
{ kind: "action", label: "ABC", actionId: "mode-alpha", width: 1.1 },
|
||||||
{ kind: "modifier", label: "Ctrl", modifier: "ctrl", width: 1.1 },
|
{ kind: "modifier", label: "Ctrl", modifier: "ctrl", width: 1.1 },
|
||||||
{ kind: "modifier", label: "Alt", modifier: "alt", width: 1.1 },
|
{ kind: "modifier", label: "Alt", modifier: "alt", width: 1.1 },
|
||||||
{ kind: "space", label: "␣", value: " ", width: 4 },
|
{ kind: "space", label: "␣", value: " ", width: 4 },
|
||||||
{ kind: "action", label: "⏎", seq: "\r", width: 1.35 },
|
{ kind: "action", label: "⏎", seq: "\r", width: 1.35 },
|
||||||
],
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
const VIRTUAL_KEYBOARD_SELECTION_LAYOUT: VirtualKeyboardKey[][] = [
|
||||||
[
|
[
|
||||||
{ kind: "arrow", label: "←", seq: "\x1b[D", width: 1.1 },
|
{ kind: "action", label: "Select", actionId: "toggle-selection-adjust", width: 1.4 },
|
||||||
{ kind: "arrow", label: "↓", seq: "\x1b[B", width: 1.1 },
|
{ kind: "action", label: "←", value: "ArrowLeft", seq: "\x1b[D" },
|
||||||
{ kind: "arrow", label: "↑", seq: "\x1b[A", width: 1.1 },
|
{ kind: "action", label: "→", value: "ArrowRight", seq: "\x1b[C" },
|
||||||
{ kind: "arrow", label: "→", seq: "\x1b[C", width: 1.1 },
|
],
|
||||||
|
[
|
||||||
|
{ kind: "action", label: "Copy", actionId: "copy-selection", width: 1.4 },
|
||||||
|
{ kind: "action", label: "Paste", actionId: "paste-selection", width: 1.4 },
|
||||||
|
{ kind: "action", label: "Cut", actionId: "cut-selection", width: 1.2 },
|
||||||
|
{ kind: "action", label: "⌫", value: "Backspace", seq: "\x7f" },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ kind: "action", label: "✂︎", actionId: "mode-alpha" },
|
||||||
|
{ kind: "space", label: "␣", value: " ", width: 2.8 },
|
||||||
|
{ kind: "action", label: "⏎", seq: "\r" },
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
type VirtualKeyboardActionId =
|
||||||
|
| "mode-alpha"
|
||||||
|
| "mode-selection"
|
||||||
|
| "mode-symbol"
|
||||||
|
| "toggle-selection-adjust"
|
||||||
|
| "copy-selection"
|
||||||
|
| "paste-selection"
|
||||||
|
| "cut-selection";
|
||||||
|
|
||||||
|
type VirtualKeyboardKeyBounds = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
rowIndex: number;
|
||||||
|
colIndex: number;
|
||||||
|
key: VirtualKeyboardKey;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ActiveVirtualKeyboardPress = {
|
||||||
|
pointerId: number;
|
||||||
|
keyIndex: number;
|
||||||
|
key: VirtualKeyboardKeyBounds | null;
|
||||||
|
repeatTimeout?: number;
|
||||||
|
};
|
||||||
|
|
||||||
const FN_NORMAL_KEYS = [
|
const FN_NORMAL_KEYS = [
|
||||||
"\x1bOP",
|
"\x1bOP",
|
||||||
"\x1bOQ",
|
"\x1bOQ",
|
||||||
@@ -972,9 +1013,14 @@ class WebTerminal {
|
|||||||
private mobileInput: HTMLTextAreaElement | null = null;
|
private mobileInput: HTMLTextAreaElement | null = null;
|
||||||
private mobileKeybar: HTMLElement | null = null;
|
private mobileKeybar: HTMLElement | null = null;
|
||||||
private mobileVirtualKeyboardHost: HTMLDivElement | null = null;
|
private mobileVirtualKeyboardHost: HTMLDivElement | null = null;
|
||||||
private mobileVirtualKeyboard: HTMLDivElement | null = null;
|
private mobileVirtualKeyboard: HTMLCanvasElement | null = null;
|
||||||
private mobileKeyboardVisible = false;
|
private mobileKeyboardVisible = false;
|
||||||
private mobileKeyboardMode: "alpha" | "symbol" = "alpha";
|
private mobileKeyboardMode: "alpha" | "symbol" = "alpha";
|
||||||
|
private mobileSelectionMode = false;
|
||||||
|
private mobileSelectionAdjusting = false;
|
||||||
|
private mobileVirtualKeyboardBounds: VirtualKeyboardKeyBounds[] = [];
|
||||||
|
private mobileVirtualKeyboardActivePresses = new Map<number, ActiveVirtualKeyboardPress>();
|
||||||
|
private mobileVirtualKeyboardDrawFrame: number | null = null;
|
||||||
private lastMobileKeyboardTouchAt = 0;
|
private lastMobileKeyboardTouchAt = 0;
|
||||||
private ctrlActive = false;
|
private ctrlActive = false;
|
||||||
private altActive = false;
|
private altActive = false;
|
||||||
@@ -2252,7 +2298,7 @@ class WebTerminal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private keybarButtonHeight = 44;
|
private keybarButtonHeight = 44;
|
||||||
private virtualKeyboardButtonHeight = 110;
|
private virtualKeyboardBaseHeight = 500;
|
||||||
|
|
||||||
private usesVirtualKeyboard(): boolean {
|
private usesVirtualKeyboard(): boolean {
|
||||||
return isMobileDevice();
|
return isMobileDevice();
|
||||||
@@ -2266,6 +2312,10 @@ class WebTerminal {
|
|||||||
if (this.mobileKeybar) {
|
if (this.mobileKeybar) {
|
||||||
this.mobileKeybar.style.display = visible ? "" : "none";
|
this.mobileKeybar.style.display = visible ? "" : "none";
|
||||||
}
|
}
|
||||||
|
if (visible) {
|
||||||
|
this.updateVirtualKeyboardHostHeight();
|
||||||
|
this.requestMobileVirtualKeyboardDraw();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private toggleMobileKeyboard(): void {
|
private toggleMobileKeyboard(): void {
|
||||||
@@ -2372,108 +2422,486 @@ class WebTerminal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private syncVirtualKeyboardState(): void {
|
private syncVirtualKeyboardState(): void {
|
||||||
if (!this.mobileVirtualKeyboard) {
|
this.requestMobileVirtualKeyboardDraw();
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.mobileVirtualKeyboard.querySelectorAll<HTMLElement>("button[data-vk-modifier]").forEach((btn) => {
|
|
||||||
const modifier = btn.dataset.vkModifier;
|
|
||||||
const active =
|
|
||||||
modifier === "ctrl"
|
|
||||||
? this.ctrlActive
|
|
||||||
: modifier === "alt"
|
|
||||||
? this.altActive
|
|
||||||
: modifier === "shift"
|
|
||||||
? this.shiftActive
|
|
||||||
: modifier === "fn"
|
|
||||||
? this.fnActive
|
|
||||||
: false;
|
|
||||||
btn.classList.toggle("webterm-vk-active", active);
|
|
||||||
});
|
|
||||||
|
|
||||||
const useShift = this.shiftActive;
|
|
||||||
this.mobileVirtualKeyboard.querySelectorAll<HTMLElement>("button[data-vk-default]").forEach((btn) => {
|
|
||||||
const label = useShift && btn.dataset.vkShiftLabel ? btn.dataset.vkShiftLabel : btn.dataset.vkDefault;
|
|
||||||
const value = useShift && btn.dataset.vkShiftValue ? btn.dataset.vkShiftValue : btn.dataset.vkValue;
|
|
||||||
btn.textContent = label || "";
|
|
||||||
if (value) {
|
|
||||||
btn.dataset.vkCurrent = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private currentVirtualKeyboardLayout(): VirtualKeyboardKey[][] {
|
private currentVirtualKeyboardLayout(): VirtualKeyboardKey[][] {
|
||||||
|
if (this.mobileSelectionMode) {
|
||||||
|
return VIRTUAL_KEYBOARD_SELECTION_LAYOUT;
|
||||||
|
}
|
||||||
return this.mobileKeyboardMode === "symbol"
|
return this.mobileKeyboardMode === "symbol"
|
||||||
? VIRTUAL_KEYBOARD_SYMBOL_LAYOUT
|
? VIRTUAL_KEYBOARD_SYMBOL_LAYOUT
|
||||||
: VIRTUAL_KEYBOARD_ALPHA_LAYOUT;
|
: VIRTUAL_KEYBOARD_ALPHA_LAYOUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderVirtualKeyboardRows(): void {
|
private getVirtualKeyboardScale(width: number): number {
|
||||||
|
return width < 390 ? 390 / width : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateVirtualKeyboardHostHeight(): void {
|
||||||
|
if (!this.mobileVirtualKeyboardHost) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const width = this.mobileVirtualKeyboardHost.getBoundingClientRect().width;
|
||||||
|
if (!width) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const scale = this.getVirtualKeyboardScale(width);
|
||||||
|
this.mobileVirtualKeyboardHost.style.height = `${Math.round(this.virtualKeyboardBaseHeight / scale)}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getVirtualKeyboardRect(): DOMRect | null {
|
||||||
|
return this.mobileVirtualKeyboard?.getBoundingClientRect() ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getVirtualKeyboardLabel(key: VirtualKeyboardKey): string {
|
||||||
|
if (key.kind === "char") {
|
||||||
|
return this.shiftActive && key.shiftLabel ? key.shiftLabel : key.label;
|
||||||
|
}
|
||||||
|
return key.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getVirtualKeyboardValue(key: VirtualKeyboardKey): string {
|
||||||
|
if (key.kind === "char") {
|
||||||
|
if (this.shiftActive && key.shiftValue) {
|
||||||
|
return key.shiftValue;
|
||||||
|
}
|
||||||
|
return key.value ?? key.label;
|
||||||
|
}
|
||||||
|
if (key.kind === "space") {
|
||||||
|
return key.value ?? " ";
|
||||||
|
}
|
||||||
|
return key.value ?? key.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateVirtualKeyboardLayout(): VirtualKeyboardKeyBounds[] {
|
||||||
|
const rect = this.getVirtualKeyboardRect();
|
||||||
|
if (!rect) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const layout = this.currentVirtualKeyboardLayout();
|
||||||
|
const scale = this.getVirtualKeyboardScale(rect.width);
|
||||||
|
const compactRatio = Math.max(0, Math.min(1, (rect.width - 260) / 140));
|
||||||
|
const padding = (2 + (10 - 2) * compactRatio) / scale;
|
||||||
|
const gap = (2 + (8 - 2) * compactRatio) / scale;
|
||||||
|
const contentW = rect.width - padding * 2;
|
||||||
|
const contentH = rect.height - padding * 2;
|
||||||
|
const rowHeight = (contentH - (layout.length - 1) * gap) / layout.length;
|
||||||
|
const bounds: VirtualKeyboardKeyBounds[] = [];
|
||||||
|
let currentY = padding;
|
||||||
|
|
||||||
|
layout.forEach((row, rowIndex) => {
|
||||||
|
const totalGapW = (row.length - 1) * gap;
|
||||||
|
const totalFlexGrow = row.reduce((sum, key) => sum + (key.width ?? 1), 0);
|
||||||
|
const unitW = (contentW - totalGapW) / totalFlexGrow;
|
||||||
|
let currentX = padding;
|
||||||
|
|
||||||
|
row.forEach((key, colIndex) => {
|
||||||
|
const width = unitW * (key.width ?? 1);
|
||||||
|
bounds.push({
|
||||||
|
x: currentX,
|
||||||
|
y: currentY,
|
||||||
|
w: width,
|
||||||
|
h: rowHeight,
|
||||||
|
rowIndex,
|
||||||
|
colIndex,
|
||||||
|
key,
|
||||||
|
label: this.getVirtualKeyboardLabel(key),
|
||||||
|
value: this.getVirtualKeyboardValue(key),
|
||||||
|
});
|
||||||
|
currentX += width + gap;
|
||||||
|
});
|
||||||
|
|
||||||
|
currentY += rowHeight + gap;
|
||||||
|
});
|
||||||
|
|
||||||
|
return bounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isVirtualKeyboardModifierActive(key: VirtualKeyboardKeyBounds): boolean {
|
||||||
|
if (key.key.modifier === "shift") {
|
||||||
|
return this.shiftActive;
|
||||||
|
}
|
||||||
|
if (key.key.modifier === "ctrl") {
|
||||||
|
return this.ctrlActive;
|
||||||
|
}
|
||||||
|
if (key.key.modifier === "alt") {
|
||||||
|
return this.altActive;
|
||||||
|
}
|
||||||
|
if (key.key.modifier === "fn") {
|
||||||
|
return this.fnActive;
|
||||||
|
}
|
||||||
|
if (key.key.actionId === "toggle-selection-adjust") {
|
||||||
|
return this.mobileSelectionAdjusting;
|
||||||
|
}
|
||||||
|
if (key.key.actionId === "mode-selection" || key.key.actionId === "mode-alpha") {
|
||||||
|
return this.mobileSelectionMode;
|
||||||
|
}
|
||||||
|
if (key.key.actionId === "mode-symbol") {
|
||||||
|
return this.mobileKeyboardMode === "symbol";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawVirtualKeyboardKey(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
key: VirtualKeyboardKeyBounds,
|
||||||
|
pressed: boolean,
|
||||||
|
active: boolean,
|
||||||
|
scale: number,
|
||||||
|
): void {
|
||||||
|
let radius = 10 / scale;
|
||||||
|
if (key.w < radius * 2) {
|
||||||
|
radius = key.w / 2;
|
||||||
|
}
|
||||||
|
if (key.h < radius * 2) {
|
||||||
|
radius = key.h / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
shadow: "#0f172a",
|
||||||
|
keyFace: pressed ? "#334155" : "#1e293b",
|
||||||
|
keyText: "#f8fafc",
|
||||||
|
actionFace: pressed ? "#475569" : "#334155",
|
||||||
|
activeFace: pressed ? "#f8fafc" : "#e2e8f0",
|
||||||
|
activeText: "#0f172a",
|
||||||
|
};
|
||||||
|
|
||||||
|
const isAction = key.key.kind === "action" || key.key.kind === "arrow" || key.key.kind === "modifier";
|
||||||
|
const faceColor = active ? colors.activeFace : isAction ? colors.actionFace : colors.keyFace;
|
||||||
|
const textColor = active ? colors.activeText : colors.keyText;
|
||||||
|
const depth = 4 / scale;
|
||||||
|
const pressOffset = pressed ? depth * 0.6 : 0;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(key.x + radius, key.y + depth);
|
||||||
|
ctx.arcTo(key.x + key.w, key.y + depth, key.x + key.w, key.y + key.h + depth, radius);
|
||||||
|
ctx.arcTo(key.x + key.w, key.y + key.h + depth, key.x, key.y + key.h + depth, radius);
|
||||||
|
ctx.arcTo(key.x, key.y + key.h + depth, key.x, key.y + depth, radius);
|
||||||
|
ctx.arcTo(key.x, key.y + depth, key.x + key.w, key.y + depth, radius);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fillStyle = colors.shadow;
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(key.x + radius, key.y + pressOffset);
|
||||||
|
ctx.arcTo(key.x + key.w, key.y + pressOffset, key.x + key.w, key.y + key.h + pressOffset, radius);
|
||||||
|
ctx.arcTo(key.x + key.w, key.y + key.h + pressOffset, key.x, key.y + key.h + pressOffset, radius);
|
||||||
|
ctx.arcTo(key.x, key.y + key.h + pressOffset, key.x, key.y + pressOffset, radius);
|
||||||
|
ctx.arcTo(key.x, key.y + pressOffset, key.x + key.w, key.y + pressOffset, radius);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fillStyle = faceColor;
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
let fontSize = key.h * 0.34;
|
||||||
|
if (key.label.length > 1) {
|
||||||
|
fontSize = key.h * 0.24;
|
||||||
|
}
|
||||||
|
if (key.label.length > 5) {
|
||||||
|
fontSize = key.h * 0.2;
|
||||||
|
}
|
||||||
|
fontSize = Math.max(fontSize, 14 / scale);
|
||||||
|
|
||||||
|
ctx.fillStyle = textColor;
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
ctx.font = `500 ${fontSize}px Inter, system-ui, sans-serif`;
|
||||||
|
ctx.fillText(key.label, key.x + key.w / 2, key.y + key.h / 2 + pressOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawMobileVirtualKeyboard(): void {
|
||||||
|
const canvas = this.mobileVirtualKeyboard;
|
||||||
|
if (!canvas || !this.mobileKeyboardVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ctx = canvas.getContext("2d", { alpha: false });
|
||||||
|
if (!ctx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
if (!rect.width || !rect.height) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
const width = Math.round(rect.width * dpr);
|
||||||
|
const height = Math.round(rect.height * dpr);
|
||||||
|
if (canvas.width !== width || canvas.height !== height) {
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
}
|
||||||
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
ctx.fillStyle = "#0f172a";
|
||||||
|
ctx.fillRect(0, 0, rect.width, rect.height);
|
||||||
|
|
||||||
|
this.mobileVirtualKeyboardBounds = this.calculateVirtualKeyboardLayout();
|
||||||
|
const scale = this.getVirtualKeyboardScale(rect.width);
|
||||||
|
|
||||||
|
this.mobileVirtualKeyboardBounds.forEach((key) => {
|
||||||
|
let pressed = false;
|
||||||
|
for (const press of this.mobileVirtualKeyboardActivePresses.values()) {
|
||||||
|
const keyIndex = key.rowIndex * 100 + key.colIndex;
|
||||||
|
if (press.keyIndex === keyIndex) {
|
||||||
|
pressed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.drawVirtualKeyboardKey(
|
||||||
|
ctx,
|
||||||
|
key,
|
||||||
|
pressed,
|
||||||
|
this.isVirtualKeyboardModifierActive(key),
|
||||||
|
scale,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private requestMobileVirtualKeyboardDraw(): void {
|
||||||
|
if (this.mobileVirtualKeyboardDrawFrame !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.mobileVirtualKeyboardDrawFrame = window.requestAnimationFrame(() => {
|
||||||
|
this.mobileVirtualKeyboardDrawFrame = null;
|
||||||
|
this.drawMobileVirtualKeyboard();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearVirtualKeyboardRepeat(press: ActiveVirtualKeyboardPress | undefined): void {
|
||||||
|
if (press?.repeatTimeout) {
|
||||||
|
window.clearTimeout(press.repeatTimeout);
|
||||||
|
press.repeatTimeout = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private findVirtualKeyboardKey(clientX: number, clientY: number): VirtualKeyboardKeyBounds | null {
|
||||||
|
const rect = this.getVirtualKeyboardRect();
|
||||||
|
if (!rect) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const x = clientX - rect.left;
|
||||||
|
const y = clientY - rect.top;
|
||||||
|
return this.mobileVirtualKeyboardBounds.find((key) =>
|
||||||
|
x >= key.x && x <= key.x + key.w && y >= key.y && y <= key.y + key.h,
|
||||||
|
) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private startVirtualKeyboardRepeat(press: ActiveVirtualKeyboardPress): void {
|
||||||
|
const value = press.key?.value ?? "";
|
||||||
|
const repeatable =
|
||||||
|
press.key?.key.kind === "char" ||
|
||||||
|
press.key?.key.kind === "space" ||
|
||||||
|
value === "Backspace" ||
|
||||||
|
value === "Delete" ||
|
||||||
|
value === "ArrowLeft" ||
|
||||||
|
value === "ArrowRight";
|
||||||
|
|
||||||
|
if (!repeatable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tick = () => {
|
||||||
|
const activePress = this.mobileVirtualKeyboardActivePresses.get(press.pointerId);
|
||||||
|
if (!activePress?.key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void this.dispatchVirtualKeyboardKey(activePress.key);
|
||||||
|
activePress.repeatTimeout = window.setTimeout(tick, 60);
|
||||||
|
this.mobileVirtualKeyboardActivePresses.set(press.pointerId, activePress);
|
||||||
|
};
|
||||||
|
|
||||||
|
press.repeatTimeout = window.setTimeout(tick, 500);
|
||||||
|
this.mobileVirtualKeyboardActivePresses.set(press.pointerId, press);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async copyTerminalSelection(): Promise<void> {
|
||||||
|
const terminal = this.terminal as unknown as {
|
||||||
|
getSelection?: () => string;
|
||||||
|
hasSelection?: () => boolean;
|
||||||
|
clearSelection?: () => void;
|
||||||
|
};
|
||||||
|
const selectedText =
|
||||||
|
(terminal.hasSelection?.() ? terminal.getSelection?.() : "") ||
|
||||||
|
document.getSelection()?.toString() ||
|
||||||
|
"";
|
||||||
|
if (!selectedText) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await navigator.clipboard?.writeText(selectedText);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async pasteFromClipboard(): Promise<void> {
|
||||||
|
const text = await navigator.clipboard?.readText();
|
||||||
|
if (text) {
|
||||||
|
this.sendStdin(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async cutTerminalSelection(): Promise<void> {
|
||||||
|
const terminal = this.terminal as unknown as {
|
||||||
|
clearSelection?: () => void;
|
||||||
|
};
|
||||||
|
await this.copyTerminalSelection();
|
||||||
|
terminal.clearSelection?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendSelectionArrow(seq: "\x1b[D" | "\x1b[C"): void {
|
||||||
|
const { useCtrl, useAlt, useFn } = this.getMobileModifierState();
|
||||||
|
const useShift = this.shiftActive || this.pendingShift || this.mobileSelectionAdjusting;
|
||||||
|
let output: string = seq;
|
||||||
|
const dir = output[2];
|
||||||
|
if (useCtrl && useShift) {
|
||||||
|
output = `\x1b[1;6${dir}`;
|
||||||
|
} else if (useCtrl) {
|
||||||
|
output = `\x1b[1;5${dir}`;
|
||||||
|
} else if (useShift) {
|
||||||
|
output = `\x1b[1;2${dir}`;
|
||||||
|
}
|
||||||
|
if (useAlt) {
|
||||||
|
output = applyAltModifier(output);
|
||||||
|
}
|
||||||
|
if (useFn && output.length === 1) {
|
||||||
|
const fnApplied = applyFnModifier(output, useShift);
|
||||||
|
if (fnApplied) {
|
||||||
|
output = fnApplied;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.sendStdin(output);
|
||||||
|
this.deactivateModifiers();
|
||||||
|
this.syncVirtualKeyboardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async dispatchVirtualKeyboardKey(key: VirtualKeyboardKeyBounds): Promise<void> {
|
||||||
|
const actionId = key.key.actionId;
|
||||||
|
if (actionId === "mode-alpha") {
|
||||||
|
this.mobileSelectionMode = false;
|
||||||
|
this.mobileKeyboardMode = "alpha";
|
||||||
|
this.mobileSelectionAdjusting = false;
|
||||||
|
this.syncVirtualKeyboardState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (actionId === "mode-selection") {
|
||||||
|
this.mobileSelectionMode = true;
|
||||||
|
this.mobileSelectionAdjusting = false;
|
||||||
|
this.syncVirtualKeyboardState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (actionId === "mode-symbol") {
|
||||||
|
this.mobileSelectionMode = false;
|
||||||
|
this.mobileKeyboardMode = this.mobileKeyboardMode === "symbol" ? "alpha" : "symbol";
|
||||||
|
this.syncVirtualKeyboardState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (actionId === "toggle-selection-adjust") {
|
||||||
|
this.mobileSelectionAdjusting = !this.mobileSelectionAdjusting;
|
||||||
|
this.syncVirtualKeyboardState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (actionId === "copy-selection") {
|
||||||
|
await this.copyTerminalSelection().catch(() => undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (actionId === "paste-selection") {
|
||||||
|
await this.pasteFromClipboard().catch(() => undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (actionId === "cut-selection") {
|
||||||
|
await this.cutTerminalSelection().catch(() => undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key.key.modifier) {
|
||||||
|
this.toggleModifierState(key.key.modifier);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key.value === "ArrowLeft") {
|
||||||
|
this.sendSelectionArrow("\x1b[D");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key.value === "ArrowRight") {
|
||||||
|
this.sendSelectionArrow("\x1b[C");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key.key.seq) {
|
||||||
|
this.sendMobileSequence(key.key.seq);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.sendMobileText(key.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleVirtualKeyboardPointerDown = (event: PointerEvent): void => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
if (!this.mobileVirtualKeyboard) {
|
if (!this.mobileVirtualKeyboard) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.mobileVirtualKeyboard.replaceChildren();
|
this.mobileVirtualKeyboard.setPointerCapture(event.pointerId);
|
||||||
|
if (!this.mobileVirtualKeyboardBounds.length) {
|
||||||
this.currentVirtualKeyboardLayout().forEach((row) => {
|
this.mobileVirtualKeyboardBounds = this.calculateVirtualKeyboardLayout();
|
||||||
const rowEl = document.createElement("div");
|
}
|
||||||
rowEl.className = "webterm-vk-row";
|
this.mobileVirtualKeyboardActivePresses.forEach((press) => {
|
||||||
rowEl.dataset.rowSize = String(row.length);
|
this.clearVirtualKeyboardRepeat(press);
|
||||||
row.forEach((key) => {
|
|
||||||
const btn = document.createElement("button");
|
|
||||||
btn.type = "button";
|
|
||||||
btn.className = `webterm-vk-button webterm-vk-${key.kind}`;
|
|
||||||
btn.textContent = key.label;
|
|
||||||
btn.style.flexGrow = String(key.width ?? 1);
|
|
||||||
btn.style.flexBasis = "0";
|
|
||||||
if (key.kind === "modifier" && key.modifier) {
|
|
||||||
btn.dataset.vkModifier = key.modifier;
|
|
||||||
}
|
|
||||||
if (key.kind === "char" || key.kind === "space") {
|
|
||||||
btn.dataset.vkDefault = key.label;
|
|
||||||
btn.dataset.vkValue = key.value ?? key.label;
|
|
||||||
if (key.shiftLabel) {
|
|
||||||
btn.dataset.vkShiftLabel = key.shiftLabel;
|
|
||||||
btn.dataset.vkShiftValue = key.shiftValue ?? key.shiftLabel;
|
|
||||||
}
|
|
||||||
btn.dataset.vkCurrent = key.value ?? key.label;
|
|
||||||
}
|
|
||||||
if (key.seq) {
|
|
||||||
btn.dataset.vkSeq = key.seq;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePress = (event: Event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
if (key.actionId === "mode-alpha") {
|
|
||||||
this.mobileKeyboardMode = "alpha";
|
|
||||||
this.renderVirtualKeyboardRows();
|
|
||||||
this.syncVirtualKeyboardState();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (key.actionId === "mode-symbol") {
|
|
||||||
this.mobileKeyboardMode = "symbol";
|
|
||||||
this.renderVirtualKeyboardRows();
|
|
||||||
this.syncVirtualKeyboardState();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (key.kind === "modifier" && key.modifier) {
|
|
||||||
this.toggleModifierState(key.modifier);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (key.seq) {
|
|
||||||
this.sendMobileSequence(key.seq);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const value = btn.dataset.vkCurrent || key.value || key.label;
|
|
||||||
this.sendMobileText(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
btn.addEventListener("touchstart", handlePress, { passive: false });
|
|
||||||
btn.addEventListener("click", handlePress);
|
|
||||||
rowEl.appendChild(btn);
|
|
||||||
});
|
|
||||||
this.mobileVirtualKeyboard!.appendChild(rowEl);
|
|
||||||
});
|
});
|
||||||
}
|
const hitKey = this.findVirtualKeyboardKey(event.clientX, event.clientY);
|
||||||
|
if (hitKey) {
|
||||||
|
const press: ActiveVirtualKeyboardPress = {
|
||||||
|
pointerId: event.pointerId,
|
||||||
|
keyIndex: hitKey.rowIndex * 100 + hitKey.colIndex,
|
||||||
|
key: hitKey,
|
||||||
|
};
|
||||||
|
void this.dispatchVirtualKeyboardKey(hitKey);
|
||||||
|
this.mobileVirtualKeyboardActivePresses.set(event.pointerId, press);
|
||||||
|
this.startVirtualKeyboardRepeat(press);
|
||||||
|
} else {
|
||||||
|
this.mobileVirtualKeyboardActivePresses.set(event.pointerId, {
|
||||||
|
pointerId: event.pointerId,
|
||||||
|
keyIndex: -1,
|
||||||
|
key: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.requestMobileVirtualKeyboardDraw();
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleVirtualKeyboardPointerMove = (event: PointerEvent): void => {
|
||||||
|
event.preventDefault();
|
||||||
|
const press = this.mobileVirtualKeyboardActivePresses.get(event.pointerId);
|
||||||
|
if (!press) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hitKey = this.findVirtualKeyboardKey(event.clientX, event.clientY);
|
||||||
|
if (hitKey) {
|
||||||
|
const newKeyIndex = hitKey.rowIndex * 100 + hitKey.colIndex;
|
||||||
|
if (newKeyIndex !== press.keyIndex) {
|
||||||
|
this.clearVirtualKeyboardRepeat(press);
|
||||||
|
const nextPress: ActiveVirtualKeyboardPress = {
|
||||||
|
pointerId: event.pointerId,
|
||||||
|
keyIndex: newKeyIndex,
|
||||||
|
key: hitKey,
|
||||||
|
};
|
||||||
|
void this.dispatchVirtualKeyboardKey(hitKey);
|
||||||
|
this.mobileVirtualKeyboardActivePresses.set(event.pointerId, nextPress);
|
||||||
|
this.startVirtualKeyboardRepeat(nextPress);
|
||||||
|
}
|
||||||
|
} else if (press.keyIndex !== -1) {
|
||||||
|
this.clearVirtualKeyboardRepeat(press);
|
||||||
|
this.mobileVirtualKeyboardActivePresses.set(event.pointerId, {
|
||||||
|
pointerId: event.pointerId,
|
||||||
|
keyIndex: -1,
|
||||||
|
key: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.requestMobileVirtualKeyboardDraw();
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleVirtualKeyboardPointerUp = (event: PointerEvent): void => {
|
||||||
|
event.preventDefault();
|
||||||
|
const press = this.mobileVirtualKeyboardActivePresses.get(event.pointerId);
|
||||||
|
if (press) {
|
||||||
|
this.clearVirtualKeyboardRepeat(press);
|
||||||
|
this.mobileVirtualKeyboardActivePresses.delete(event.pointerId);
|
||||||
|
}
|
||||||
|
if (this.mobileVirtualKeyboard?.hasPointerCapture(event.pointerId)) {
|
||||||
|
this.mobileVirtualKeyboard.releasePointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
this.requestMobileVirtualKeyboardDraw();
|
||||||
|
};
|
||||||
|
|
||||||
private setupVirtualKeyboard(): void {
|
private setupVirtualKeyboard(): void {
|
||||||
if (this.mobileVirtualKeyboard || this.mobileVirtualKeyboardHost || !document.body) {
|
if (this.mobileVirtualKeyboard || this.mobileVirtualKeyboardHost || !document.body) {
|
||||||
@@ -2482,13 +2910,42 @@ class WebTerminal {
|
|||||||
|
|
||||||
const keyboardHost = document.createElement("div");
|
const keyboardHost = document.createElement("div");
|
||||||
keyboardHost.className = "mobile-virtual-keyboard";
|
keyboardHost.className = "mobile-virtual-keyboard";
|
||||||
|
keyboardHost.style.cssText = `
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #0f172a;
|
||||||
|
padding: 0;
|
||||||
|
touch-action: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.style.cssText = `
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
touch-action: none;
|
||||||
|
`;
|
||||||
|
canvas.addEventListener("pointerdown", this.handleVirtualKeyboardPointerDown, { passive: false });
|
||||||
|
canvas.addEventListener("pointermove", this.handleVirtualKeyboardPointerMove, { passive: false });
|
||||||
|
canvas.addEventListener("pointerup", this.handleVirtualKeyboardPointerUp, { passive: false });
|
||||||
|
canvas.addEventListener("pointercancel", this.handleVirtualKeyboardPointerUp, { passive: false });
|
||||||
|
canvas.addEventListener("pointerleave", this.handleVirtualKeyboardPointerUp, { passive: false });
|
||||||
|
|
||||||
|
keyboardHost.appendChild(canvas);
|
||||||
document.body.appendChild(keyboardHost);
|
document.body.appendChild(keyboardHost);
|
||||||
this.mobileVirtualKeyboardHost = keyboardHost;
|
this.mobileVirtualKeyboardHost = keyboardHost;
|
||||||
this.mobileVirtualKeyboard = keyboardHost;
|
this.mobileVirtualKeyboard = canvas;
|
||||||
this.renderVirtualKeyboardRows();
|
this.mobileVirtualKeyboardBounds = [];
|
||||||
this.syncVirtualKeyboardState();
|
this.updateVirtualKeyboardHostHeight();
|
||||||
|
this.addTrackedListener(window, "resize", () => {
|
||||||
|
this.updateVirtualKeyboardHostHeight();
|
||||||
|
this.mobileVirtualKeyboardBounds = [];
|
||||||
|
this.requestMobileVirtualKeyboardDraw();
|
||||||
|
});
|
||||||
this.setMobileKeyboardVisible(false);
|
this.setMobileKeyboardVisible(false);
|
||||||
this.clearUserError();
|
this.clearUserError();
|
||||||
|
this.requestMobileVirtualKeyboardDraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Setup bottom-docked mobile extended keyboard bar */
|
/** Setup bottom-docked mobile extended keyboard bar */
|
||||||
@@ -2499,11 +2956,15 @@ class WebTerminal {
|
|||||||
const keysPanel = document.createElement("div");
|
const keysPanel = document.createElement("div");
|
||||||
keysPanel.className = "keybar-panel keybar-keys";
|
keysPanel.className = "keybar-panel keybar-keys";
|
||||||
keysPanel.innerHTML = `
|
keysPanel.innerHTML = `
|
||||||
<button class="keybar-hide" title="Hide keyboard">⌄</button>
|
|
||||||
<button data-modifier="fn" title="Fn modifier">Fn</button>
|
<button data-modifier="fn" title="Fn modifier">Fn</button>
|
||||||
<button data-key="\\x09" title="Tab">Tab</button>
|
<button data-key="\\x09" title="Tab">Tab</button>
|
||||||
|
<button data-key="\\x1b[D" title="Left arrow">←</button>
|
||||||
|
<button data-key="\\x1b[B" title="Down arrow">↓</button>
|
||||||
|
<button data-key="\\x1b[A" title="Up arrow">↑</button>
|
||||||
|
<button data-key="\\x1b[C" title="Right arrow">→</button>
|
||||||
<span class="keybar-label keybar-label-grow">Bar</span>
|
<span class="keybar-label keybar-label-grow">Bar</span>
|
||||||
<button class="keybar-settings" title="Settings">⚙</button>
|
<button class="keybar-settings" title="Settings">⚙</button>
|
||||||
|
<button class="keybar-hide" title="Hide keyboard">⌄</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const settingsPanel = document.createElement("div");
|
const settingsPanel = document.createElement("div");
|
||||||
@@ -2536,50 +2997,13 @@ class WebTerminal {
|
|||||||
.mobile-virtual-keyboard {
|
.mobile-virtual-keyboard {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: #0f172a;
|
background: #0f172a;
|
||||||
padding: 10px 10px 8px;
|
border-top: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
}
|
}
|
||||||
.mobile-virtual-keyboard {
|
.mobile-virtual-keyboard canvas {
|
||||||
font-family: system-ui, sans-serif;
|
display: block;
|
||||||
}
|
width: 100%;
|
||||||
.mobile-virtual-keyboard .webterm-vk-row {
|
height: 100%;
|
||||||
display: flex;
|
touch-action: none;
|
||||||
gap: 6px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
.mobile-virtual-keyboard .webterm-vk-row[data-row-size="9"] {
|
|
||||||
padding-inline: 4%;
|
|
||||||
}
|
|
||||||
.mobile-virtual-keyboard .webterm-vk-row[data-row-size="4"] {
|
|
||||||
padding-inline: 10%;
|
|
||||||
}
|
|
||||||
.mobile-virtual-keyboard .webterm-vk-button {
|
|
||||||
align-items: center;
|
|
||||||
background: #1e293b;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 4px 0 #020617;
|
|
||||||
color: #f8fafc;
|
|
||||||
display: flex;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 500;
|
|
||||||
height: ${this.virtualKeyboardButtonHeight}px;
|
|
||||||
justify-content: center;
|
|
||||||
min-width: 0;
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
.mobile-virtual-keyboard .webterm-vk-button:active {
|
|
||||||
background: #334155;
|
|
||||||
transform: translateY(2px);
|
|
||||||
box-shadow: 0 2px 0 #020617;
|
|
||||||
}
|
|
||||||
.mobile-virtual-keyboard .webterm-vk-button.webterm-vk-action,
|
|
||||||
.mobile-virtual-keyboard .webterm-vk-button.webterm-vk-arrow {
|
|
||||||
background: #334155;
|
|
||||||
}
|
|
||||||
.mobile-virtual-keyboard .webterm-vk-button.webterm-vk-active {
|
|
||||||
background: #e2e8f0;
|
|
||||||
color: #0f172a;
|
|
||||||
box-shadow: 0 4px 0 #94a3b8;
|
|
||||||
}
|
}
|
||||||
.keybar-panel {
|
.keybar-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -2664,9 +3088,7 @@ class WebTerminal {
|
|||||||
keybar.querySelectorAll("button").forEach((btn) => {
|
keybar.querySelectorAll("button").forEach((btn) => {
|
||||||
(btn as HTMLElement).style.height = `${this.keybarButtonHeight}px`;
|
(btn as HTMLElement).style.height = `${this.keybarButtonHeight}px`;
|
||||||
});
|
});
|
||||||
this.mobileVirtualKeyboardHost?.querySelectorAll(".webterm-vk-button").forEach((btn) => {
|
this.requestMobileVirtualKeyboardDraw();
|
||||||
(btn as HTMLElement).style.height = `${this.virtualKeyboardButtonHeight}px`;
|
|
||||||
});
|
|
||||||
this.fit();
|
this.fit();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3164,6 +3586,14 @@ class WebTerminal {
|
|||||||
clearTimeout(this.pendingStdinTimer);
|
clearTimeout(this.pendingStdinTimer);
|
||||||
this.pendingStdinTimer = undefined;
|
this.pendingStdinTimer = undefined;
|
||||||
}
|
}
|
||||||
|
if (this.mobileVirtualKeyboardDrawFrame !== null) {
|
||||||
|
cancelAnimationFrame(this.mobileVirtualKeyboardDrawFrame);
|
||||||
|
this.mobileVirtualKeyboardDrawFrame = null;
|
||||||
|
}
|
||||||
|
this.mobileVirtualKeyboardActivePresses.forEach((press) => {
|
||||||
|
this.clearVirtualKeyboardRepeat(press);
|
||||||
|
});
|
||||||
|
this.mobileVirtualKeyboardActivePresses.clear();
|
||||||
this.pendingStdin = "";
|
this.pendingStdin = "";
|
||||||
if (this.resizeDebounceTimer) {
|
if (this.resizeDebounceTimer) {
|
||||||
clearTimeout(this.resizeDebounceTimer);
|
clearTimeout(this.resizeDebounceTimer);
|
||||||
@@ -3200,6 +3630,7 @@ class WebTerminal {
|
|||||||
this.mobileVirtualKeyboardHost.remove();
|
this.mobileVirtualKeyboardHost.remove();
|
||||||
this.mobileVirtualKeyboardHost = null;
|
this.mobileVirtualKeyboardHost = null;
|
||||||
}
|
}
|
||||||
|
this.mobileVirtualKeyboardBounds = [];
|
||||||
if (this.mobileKeybarStyle) {
|
if (this.mobileKeybarStyle) {
|
||||||
this.mobileKeybarStyle.remove();
|
this.mobileKeybarStyle.remove();
|
||||||
this.mobileKeybarStyle = null;
|
this.mobileKeybarStyle = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user