@@ -121,8 +121,12 @@ <h3 id="vm-name">VM Console</h3>
121121 < span id ="status-badge " class ="badge disconnected "> Connecting...</ span >
122122 < div class ="spacer "> </ div >
123123 < button onclick ="sendCtrlAltDel() " class ="ctrl-alt-del " title ="Send Ctrl+Alt+Del "> Ctrl+Alt+Del</ button >
124+ < button onclick ="toggleKeyboard() " id ="kb-btn " title ="Show on-screen keyboard "> Keyboard</ button >
124125 < button onclick ="toggleFullscreen() "> Fullscreen</ button >
125126 </ div >
127+ <!-- Hidden textarea to trigger iOS/mobile soft keyboard -->
128+ < textarea id ="mobile-kb-input " autocapitalize ="off " autocomplete ="off " autocorrect ="off " spellcheck ="false "
129+ style ="position:fixed;top:-9999px;left:-9999px;width:1px;height:1px;opacity:0;font-size:16px; "> </ textarea >
126130 < div id ="vnc-container " style ="position: relative; ">
127131 < div class ="status-overlay " id ="status-overlay ">
128132 < div class ="spinner "> </ div >
@@ -325,6 +329,62 @@ <h3 id="vm-name">VM Console</h3>
325329 document . exitFullscreen ( ) ;
326330 }
327331 } ;
332+
333+ // Mobile soft keyboard support (iOS/Android)
334+ const kbInput = document . getElementById ( 'mobile-kb-input' ) ;
335+ let kbOpen = false ;
336+
337+ window . toggleKeyboard = function ( ) {
338+ if ( kbOpen ) {
339+ kbInput . blur ( ) ;
340+ kbOpen = false ;
341+ document . getElementById ( 'kb-btn' ) . style . background = '' ;
342+ } else {
343+ kbInput . focus ( ) ;
344+ kbOpen = true ;
345+ document . getElementById ( 'kb-btn' ) . style . background = '#065f46' ;
346+ }
347+ } ;
348+
349+ kbInput . addEventListener ( 'blur' , ( ) => {
350+ kbOpen = false ;
351+ document . getElementById ( 'kb-btn' ) . style . background = '' ;
352+ } ) ;
353+
354+ // Forward key events from the hidden textarea to noVNC
355+ kbInput . addEventListener ( 'keydown' , ( e ) => {
356+ if ( ! rfb ) return ;
357+ // Let noVNC handle the key via its own canvas focus for special keys
358+ const key = e . key ;
359+ if ( key === 'Enter' ) rfb . sendKey ( 0xFF0D ) ;
360+ else if ( key === 'Backspace' ) rfb . sendKey ( 0xFF08 ) ;
361+ else if ( key === 'Tab' ) rfb . sendKey ( 0xFF09 ) ;
362+ else if ( key === 'Escape' ) rfb . sendKey ( 0xFF1B ) ;
363+ else if ( key === 'ArrowLeft' ) rfb . sendKey ( 0xFF51 ) ;
364+ else if ( key === 'ArrowUp' ) rfb . sendKey ( 0xFF52 ) ;
365+ else if ( key === 'ArrowRight' ) rfb . sendKey ( 0xFF53 ) ;
366+ else if ( key === 'ArrowDown' ) rfb . sendKey ( 0xFF54 ) ;
367+ else if ( key === 'Delete' ) rfb . sendKey ( 0xFFFF ) ;
368+ else if ( key === 'Home' ) rfb . sendKey ( 0xFF50 ) ;
369+ else if ( key === 'End' ) rfb . sendKey ( 0xFF57 ) ;
370+ else return ; // printable chars handled via input event
371+ e . preventDefault ( ) ;
372+ } ) ;
373+
374+ // Capture printable characters via input event (works on iOS)
375+ kbInput . addEventListener ( 'input' , ( e ) => {
376+ if ( ! rfb ) return ;
377+ const data = e . data ;
378+ if ( data ) {
379+ for ( const ch of data ) {
380+ const code = ch . codePointAt ( 0 ) ;
381+ rfb . sendKey ( code , null , true ) ; // press
382+ rfb . sendKey ( code , null , false ) ; // release
383+ }
384+ }
385+ // Clear the textarea so it stays ready for more input
386+ kbInput . value = '' ;
387+ } ) ;
328388 </ script >
329389</ body >
330390
0 commit comments