mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-22 14:06:02 +00:00
Added comprehensive IME composition support to prevent duplication of Japanese, Chinese, and Korean text input on mobile devices. Changes: - DirectKeyboardManager: Added composition event handlers to wait for final text - MobileInputOverlay: Added composition state tracking to prevent intermediate updates - Fixed null safety issues and unused parameter warnings - Comprehensive documentation explaining IME implementation Bug Fixed: Previously, intermediate composition characters were sent to terminal during Japanese typing, causing duplicated text. Now properly waits for composition completion before sending final text to terminal. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b98a8d190e
commit
6e98fa9ce6
2 changed files with 127 additions and 4 deletions
|
|
@ -2,7 +2,29 @@
|
|||
* Direct Keyboard Input Manager
|
||||
*
|
||||
* Manages hidden input element and direct keyboard input for mobile devices.
|
||||
* Handles focus management, input events, and quick key interactions.
|
||||
* Handles focus management, input events, quick key interactions, and IME composition.
|
||||
*
|
||||
* ## IME Support for Japanese/CJK Input
|
||||
*
|
||||
* This manager now includes full support for Input Method Editor (IME) composition,
|
||||
* which is essential for Japanese, Chinese, and Korean text input on mobile devices.
|
||||
*
|
||||
* **How IME Works:**
|
||||
* 1. User types "konnichiwa" on Japanese keyboard
|
||||
* 2. Browser shows composition UI: "こんにちは" (intermediate characters)
|
||||
* 3. User selects final text from candidates
|
||||
* 4. Browser fires `compositionend` with final text: "こんにちは"
|
||||
*
|
||||
* **Bug Fixed (GitHub #99):**
|
||||
* Previously, intermediate composition characters were sent to terminal during typing,
|
||||
* causing duplicated/garbled Japanese text. Now we properly wait for composition
|
||||
* completion before sending any text to the terminal.
|
||||
*
|
||||
* **Implementation:**
|
||||
* - `compositionstart`: Sets isComposing=true, prevents input events from sending text
|
||||
* - `compositionupdate`: Tracks intermediate composition (for logging/debugging)
|
||||
* - `compositionend`: Sends only the final composed text to terminal
|
||||
* - `input`: Skipped entirely during composition, normal handling otherwise
|
||||
*/
|
||||
import { createLogger } from '../../utils/logger.js';
|
||||
import type { InputManager } from './input-manager.js';
|
||||
|
|
@ -37,6 +59,10 @@ export class DirectKeyboardManager {
|
|||
private keyboardActivationTimeout: number | null = null;
|
||||
private captureClickHandler: ((e: Event) => void) | null = null;
|
||||
|
||||
// IME composition state tracking for Japanese/CJK input
|
||||
private isComposing = false;
|
||||
private compositionBuffer = '';
|
||||
|
||||
constructor(instanceId: string) {
|
||||
this.instanceId = instanceId;
|
||||
}
|
||||
|
|
@ -198,15 +224,56 @@ export class DirectKeyboardManager {
|
|||
// Set initial position based on mode
|
||||
this.updateHiddenInputPosition();
|
||||
|
||||
// Handle input events
|
||||
// Handle IME composition events for Japanese/CJK input
|
||||
this.hiddenInput.addEventListener('compositionstart', () => {
|
||||
this.isComposing = true;
|
||||
this.compositionBuffer = '';
|
||||
});
|
||||
|
||||
this.hiddenInput.addEventListener('compositionupdate', (e) => {
|
||||
const compositionEvent = e as CompositionEvent;
|
||||
this.compositionBuffer = compositionEvent.data || '';
|
||||
});
|
||||
|
||||
this.hiddenInput.addEventListener('compositionend', (e) => {
|
||||
const compositionEvent = e as CompositionEvent;
|
||||
this.isComposing = false;
|
||||
|
||||
// Get the final composed text
|
||||
const finalText = compositionEvent.data || this.hiddenInput?.value || '';
|
||||
|
||||
if (finalText) {
|
||||
// Don't send input to terminal if mobile input overlay or Ctrl overlay is visible
|
||||
const showMobileInput = this.callbacks?.getShowMobileInput() ?? false;
|
||||
const showCtrlAlpha = this.callbacks?.getShowCtrlAlpha() ?? false;
|
||||
if (!showMobileInput && !showCtrlAlpha && this.inputManager) {
|
||||
// Send the completed composition to terminal
|
||||
this.inputManager.sendInputText(finalText);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the input and composition buffer
|
||||
if (this.hiddenInput) {
|
||||
this.hiddenInput.value = '';
|
||||
}
|
||||
this.compositionBuffer = '';
|
||||
});
|
||||
|
||||
// Handle input events (non-composition)
|
||||
this.hiddenInput.addEventListener('input', (e) => {
|
||||
const input = e.target as HTMLInputElement;
|
||||
|
||||
// Skip processing if we're in the middle of IME composition
|
||||
if (this.isComposing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.value) {
|
||||
// Don't send input to terminal if mobile input overlay or Ctrl overlay is visible
|
||||
const showMobileInput = this.callbacks?.getShowMobileInput() ?? false;
|
||||
const showCtrlAlpha = this.callbacks?.getShowCtrlAlpha() ?? false;
|
||||
if (!showMobileInput && !showCtrlAlpha && this.inputManager) {
|
||||
// Send each character to terminal
|
||||
// Send each character to terminal (only for non-IME input)
|
||||
this.inputManager.sendInputText(input.value);
|
||||
}
|
||||
// Always clear the input to prevent buffer buildup
|
||||
|
|
|
|||
|
|
@ -2,7 +2,23 @@
|
|||
* Mobile Input Overlay Component
|
||||
*
|
||||
* Full-screen overlay for mobile text input with virtual keyboard support.
|
||||
* Handles text input, command sending, and keyboard height adjustments.
|
||||
* Handles text input, command sending, keyboard height adjustments, and IME composition.
|
||||
*
|
||||
* ## IME Support for Japanese/CJK Input
|
||||
*
|
||||
* This overlay includes full support for Input Method Editor (IME) composition
|
||||
* for Japanese, Chinese, and Korean text input on mobile devices.
|
||||
*
|
||||
* **Bug Fixed (GitHub #99):**
|
||||
* Added proper IME composition event handling to prevent duplication of
|
||||
* Japanese text during typing. The overlay now waits for composition
|
||||
* completion before updating the text state.
|
||||
*
|
||||
* **Implementation:**
|
||||
* - `compositionstart`: Sets isComposing=true, prevents input change handling
|
||||
* - `compositionupdate`: Tracks intermediate composition text
|
||||
* - `compositionend`: Updates text state only with final composed text
|
||||
* - `input`: Skipped during composition, normal handling otherwise
|
||||
*/
|
||||
import { html, LitElement } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
|
|
@ -28,6 +44,10 @@ export class MobileInputOverlay extends LitElement {
|
|||
@property({ type: Function }) onTextChange?: (text: string) => void;
|
||||
@property({ type: Function }) handleBack?: () => void;
|
||||
|
||||
// IME composition state tracking for Japanese/CJK input
|
||||
private isComposing = false;
|
||||
private compositionBuffer = '';
|
||||
|
||||
private touchStartHandler = (e: TouchEvent) => {
|
||||
const touch = e.touches[0];
|
||||
this.touchStartX = touch.clientX;
|
||||
|
|
@ -54,12 +74,45 @@ export class MobileInputOverlay extends LitElement {
|
|||
|
||||
private handleMobileInputChange(e: Event) {
|
||||
const textarea = e.target as HTMLTextAreaElement;
|
||||
|
||||
// Skip processing if we're in the middle of IME composition
|
||||
if (this.isComposing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.mobileInputText = textarea.value;
|
||||
this.onTextChange?.(textarea.value);
|
||||
// Force update to ensure button states update
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private handleCompositionStart = (_e: CompositionEvent) => {
|
||||
this.isComposing = true;
|
||||
this.compositionBuffer = '';
|
||||
};
|
||||
|
||||
private handleCompositionUpdate = (e: CompositionEvent) => {
|
||||
this.compositionBuffer = e.data || '';
|
||||
};
|
||||
|
||||
private handleCompositionEnd = (e: CompositionEvent) => {
|
||||
this.isComposing = false;
|
||||
|
||||
// Get the final composed text
|
||||
const finalText = e.data || '';
|
||||
|
||||
// Update the mobile input text with the final composition
|
||||
const textarea = e.target as HTMLTextAreaElement;
|
||||
if (textarea && finalText) {
|
||||
this.mobileInputText = textarea.value;
|
||||
this.onTextChange?.(textarea.value);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
// Clear composition buffer
|
||||
this.compositionBuffer = '';
|
||||
};
|
||||
|
||||
private focusMobileTextarea() {
|
||||
const textarea = this.querySelector('#mobile-input-textarea') as HTMLTextAreaElement;
|
||||
if (!textarea) return;
|
||||
|
|
@ -187,6 +240,9 @@ export class MobileInputOverlay extends LitElement {
|
|||
@focus=${this.handleFocus}
|
||||
@blur=${this.handleBlur}
|
||||
@keydown=${this.handleKeydown}
|
||||
@compositionstart=${this.handleCompositionStart}
|
||||
@compositionupdate=${this.handleCompositionUpdate}
|
||||
@compositionend=${this.handleCompositionEnd}
|
||||
style="height: 120px; background: black; color: #d4d4d4; border: none; padding: 12px;"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
|
|
|
|||
Loading…
Reference in a new issue