mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-19 13:35:54 +00:00
5.7 KiB
5.7 KiB
iOS Safari Paste Implementation
Overview
This document describes the implementation of paste functionality for iOS Safari in VibeTunnel, addressing the limitations of the Clipboard API on mobile Safari and providing a reliable fallback mechanism.
The Problem
Primary Issues
-
Clipboard API Limitations on iOS Safari
navigator.clipboard.readText()only works in secure contexts (HTTPS/localhost)- Even in secure contexts, it requires "transient user activation" (immediate user gesture)
- iOS 15 and older versions don't expose
readText()at all - In HTTP contexts,
navigator.clipboardis completely undefined
-
Focus Management Conflicts
- VibeTunnel uses aggressive focus retention for the hidden input field
- Focus retention runs every 100ms to maintain keyboard visibility
- "Keyboard mode" forces focus back to hidden input continuously
- This prevents any other element from maintaining focus long enough for iOS paste menu
-
iOS Native Paste Menu Requirements
- Requires a visible, focusable text input element
- Element must maintain focus for the paste menu to appear
- User must be able to long-press the element
- Hidden or off-screen elements don't trigger the paste menu reliably
Failed Approaches
-
Off-screen Textarea (Apple's documented approach)
- Created textarea at
position: fixed; left: -9999px - Focus was immediately stolen by focus retention mechanism
- Even after disabling focus retention, keyboard mode continued stealing focus
- iOS couldn't show paste menu on an off-screen element
- Created textarea at
-
Temporary Focus Retention Disable
- Attempted to pause focus retention interval during paste
- Keyboard mode's focus management still interfered
- Complex state management led to race conditions
- Restoration of focus states was unreliable
The Solution
Implementation Strategy
Use the existing hidden input field and temporarily make it visible for paste operations:
private triggerNativePasteWithHiddenInput(): void {
// 1. Save original styles
const originalStyles = {
position: this.hiddenInput.style.position,
opacity: this.hiddenInput.style.opacity,
// ... all other styles
};
// 2. Make input visible at screen center
this.hiddenInput.style.position = 'fixed';
this.hiddenInput.style.left = '50%';
this.hiddenInput.style.top = '50%';
this.hiddenInput.style.transform = 'translate(-50%, -50%)';
this.hiddenInput.style.width = '200px';
this.hiddenInput.style.height = '40px';
this.hiddenInput.style.opacity = '1';
this.hiddenInput.style.backgroundColor = 'white';
this.hiddenInput.style.border = '2px solid #007AFF';
this.hiddenInput.style.borderRadius = '8px';
this.hiddenInput.style.padding = '8px';
this.hiddenInput.style.zIndex = '10000';
this.hiddenInput.placeholder = 'Long-press to paste';
// 3. Add paste event listener
this.hiddenInput.addEventListener('paste', handlePasteEvent);
// 4. Focus and select
this.hiddenInput.focus();
this.hiddenInput.select();
// 5. Clean up after paste or timeout
}
Why This Works
- No Focus Conflicts: Uses the same input that already has focus management
- Visible Target: iOS can show paste menu on a visible, centered element
- User-Friendly: Clear visual feedback with "Long-press to paste" placeholder
- Simple State Management: Just style changes, no complex focus juggling
- Maintains User Gesture Context: Called directly from touch event handler
User Flow
- User taps "Paste" button in quick keys
- Hidden input becomes visible at screen center with blue border
- User long-presses the visible input
- iOS shows native paste menu
- User taps "Paste" from menu
- Text is pasted and sent to terminal
- Input returns to hidden state
Implementation Details
Key Files
web/src/client/components/session-view/direct-keyboard-manager.ts:695-784- Main paste implementationweb/src/client/components/terminal-quick-keys.ts:198-207- Paste button handler
Fallback Logic
// In handleQuickKeyPress for 'Paste' key:
1. Try modern Clipboard API if available (HTTPS contexts)
2. If that fails or unavailable, use triggerNativePasteWithHiddenInput()
3. Show visible input for native iOS paste menu
Autocorrect Disable
To prevent iOS text editing interference, the hidden input has comprehensive attributes:
this.hiddenInput.autocapitalize = 'none';
this.hiddenInput.autocomplete = 'off';
this.hiddenInput.setAttribute('autocorrect', 'off');
this.hiddenInput.setAttribute('spellcheck', 'false');
this.hiddenInput.setAttribute('data-autocorrect', 'off');
this.hiddenInput.setAttribute('data-gramm', 'false');
this.hiddenInput.setAttribute('data-ms-editor', 'false');
this.hiddenInput.setAttribute('data-smartpunctuation', 'false');
this.hiddenInput.setAttribute('inputmode', 'text');
Testing
Test Scenarios
- HTTPS Context: Clipboard API should work directly
- HTTP Context: Should fall back to visible input method
- Focus Retention Active: Paste should still work without conflicts
- Multiple Paste Operations: Each should work independently
- Timeout Handling: Input should restore after 10 seconds if no paste
Known Limitations
- Requires user to long-press and select paste (two taps total)
- Shows visible UI element temporarily
- 10-second timeout if user doesn't paste
- Only works with text content (no rich text/images)
References
- iOS Safari Clipboard API Documentation
- WebKit Bug Tracker - Clipboard API Issues
- Original working implementation: Commit
44f69c45a - Issue #317: Safari paste functionality