Skip to content

Commit dc2ef37

Browse files
authored
Add mobile keyboard API (#21)
* fix page for mobile - minor changes. * fix textarea overlay to hide caret and avodi zooming on mobiles. * fix typo. * show keyboard btn if is touch device. * lint fix. * add to API. * mobile keybaord fix andorid blur. * add mobile keybaord toggle. * fix overlay. * mobile keybaord, skip if not a touch device.
1 parent 5758350 commit dc2ef37

File tree

6 files changed

+116
-13
lines changed

6 files changed

+116
-13
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ echo \"@demodesk:registry\" \"https://npm.pkg.github.com\" >> .yarnrc
2626
You can set keyboard provider at build time, either `novnc` or the default `guacamole`.
2727

2828
```bash
29-
# by default uses guacamole keybaord
29+
# by default uses guacamole keyboard
3030
npm run build
31-
# uses novnc keybaord
31+
# uses novnc keyboard
3232
KEYBOARD=novnc npm run build
3333
```
3434

src/component/main.vue

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
:cursorDraw="inactiveCursorDrawFunction"
2121
/>
2222
<neko-overlay
23+
ref="overlay"
2324
v-show="!private_mode_enabled && state.connection.status != 'disconnected'"
2425
:style="{ pointerEvents: state.control.locked ? 'none' : 'auto' }"
2526
:wsControl="control"
@@ -35,6 +36,7 @@
3536
:inactiveCursors="state.settings.inactive_cursors && session.profile.sends_inactive_cursor"
3637
@updateKeyboardModifiers="updateKeyboardModifiers($event)"
3738
@uploadDrop="uploadDrop($event)"
39+
@mobileKeyboardOpen="state.mobile_keyboard_open = $event"
3840
/>
3941
</div>
4042
</div>
@@ -102,6 +104,7 @@
102104
@Ref('component') readonly _component!: HTMLElement
103105
@Ref('container') readonly _container!: HTMLElement
104106
@Ref('video') readonly _video!: HTMLVideoElement
107+
@Ref('overlay') readonly _overlay!: Overlay
105108
106109
// fallback image for webrtc reconnections:
107110
// chrome shows black screen when closing webrtc connection, that's why
@@ -196,6 +199,7 @@
196199
merciful_reconnect: false,
197200
},
198201
cursors: [],
202+
mobile_keyboard_open: false,
199203
} as NekoState
200204
201205
/////////////////////////////
@@ -405,6 +409,22 @@
405409
Vue.set(this.state.control, 'keyboard', { layout, variant })
406410
}
407411
412+
public mobileKeyboardShow() {
413+
this._overlay.mobileKeyboardShow()
414+
}
415+
416+
public mobileKeyboardHide() {
417+
this._overlay.mobileKeyboardHide()
418+
}
419+
420+
public mobileKeyboardToggle() {
421+
if (this.state.mobile_keyboard_open) {
422+
this.mobileKeyboardHide()
423+
} else {
424+
this.mobileKeyboardShow()
425+
}
426+
}
427+
408428
public setCursorDrawFunction(fn?: CursorDrawFunction) {
409429
Vue.set(this, 'cursorDrawFunction', fn)
410430
}

src/component/overlay.vue

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@
3333
bottom: 0;
3434
width: 100%;
3535
height: 100%;
36-
font-size: 1px; /* chrome would not paste text if 0px */
36+
font-size: 16px; /* at least 16px to avoid zooming on mobile */
3737
resize: none; /* hide textarea resize corner */
38+
caret-color: transparent; /* hide caret */
3839
outline: 0;
3940
border: 0;
4041
color: transparent;
@@ -116,6 +117,10 @@
116117
return 'url(' + uri + ') ' + x + ' ' + y + ', default'
117118
}
118119
120+
get isTouchDevice(): boolean {
121+
return 'ontouchstart' in window || navigator.maxTouchPoints > 0
122+
}
123+
119124
mounted() {
120125
// register mouseup globally as user can release mouse button outside of overlay
121126
window.addEventListener('mouseup', this.onMouseUp, true)
@@ -373,7 +378,11 @@
373378
}
374379
375380
onMouseEnter(e: MouseEvent) {
376-
this._textarea.focus()
381+
// focus opens the keyboard on mobile (only for android)
382+
if (!this.isTouchDevice) {
383+
this._textarea.focus()
384+
}
385+
377386
this.focused = true
378387
379388
if (this.isControling) {
@@ -629,5 +638,48 @@
629638
this.wsControl.release()
630639
}
631640
}
641+
642+
//
643+
// mobile keyboard
644+
//
645+
646+
public kbdShow = false
647+
public kbdOpen = false
648+
649+
public mobileKeyboardShow() {
650+
// skip if not a touch device
651+
if (!this.isTouchDevice) return
652+
653+
this.kbdShow = true
654+
this.kbdOpen = false
655+
656+
this._textarea.focus()
657+
window.visualViewport.addEventListener('resize', this.onVisualViewportResize)
658+
this.$emit('mobileKeyboardOpen', true)
659+
}
660+
661+
public mobileKeyboardHide() {
662+
// skip if not a touch device
663+
if (!this.isTouchDevice) return
664+
665+
this.kbdShow = false
666+
this.kbdOpen = false
667+
668+
this.$emit('mobileKeyboardOpen', false)
669+
window.visualViewport.removeEventListener('resize', this.onVisualViewportResize)
670+
this._textarea.blur()
671+
}
672+
673+
// visual viewport resize event is fired when keyboard is opened or closed
674+
// android does not blur textarea when keyboard is closed, so we need to do it manually
675+
onVisualViewportResize() {
676+
if (!this.kbdShow) return
677+
678+
if (!this.kbdOpen) {
679+
this.kbdOpen = true
680+
} else {
681+
this.mobileKeyboardHide()
682+
}
683+
}
632684
}
633685
</script>

src/component/types/state.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export default interface State {
1111
sessions: Record<string, Session>
1212
settings: Settings
1313
cursors: Cursors
14+
mobile_keyboard_open: boolean
1415
}
1516

1617
/////////////////////////////

src/page/components/events.vue

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,18 @@
400400
<td>{{ neko.state.cursors }}</td>
401401
</tr>
402402

403+
<tr>
404+
<th>mobile_keyboard_open</th>
405+
<td>
406+
<div class="space-between">
407+
<span>{{ neko.state.mobile_keyboard_open }}</span>
408+
<button @click="neko.mobileKeyboardToggle">
409+
<i class="fas fa-toggle-on"></i>
410+
</button>
411+
</div>
412+
</td>
413+
</tr>
414+
403415
<tr>
404416
<th>control actions</th>
405417
<td>

src/page/main.vue

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@
4343
</div>
4444
</div>
4545
<div class="room-container" style="text-align: center">
46+
<button
47+
v-if="loaded && isTouchDevice"
48+
@click="neko.mobileKeyboardToggle"
49+
style="position: absolute; left: 5px; transform: translateY(-100%)"
50+
>
51+
<i class="fa fa-keyboard" />
52+
</button>
4653
<span v-if="loaded && neko.state.session_id" style="padding-top: 10px">
4754
You are logged in as
4855
<strong style="font-weight: bold">
@@ -193,6 +200,9 @@
193200
flex-shrink: 0;
194201
flex-direction: column;
195202
display: flex;
203+
/* for mobile */
204+
overflow-y: hidden;
205+
overflow-x: auto;
196206
197207
.room-menu {
198208
max-width: 100%;
@@ -279,10 +289,14 @@
279289
}
280290
}
281291
292+
/* for mobile */
282293
@media only screen and (max-width: 600px) {
294+
$offset: 38px;
295+
283296
#neko.expanded {
297+
/* show only enough of the menu to see the toggle button */
284298
.neko-main {
285-
transform: translateX(calc(-100% + 65px));
299+
transform: translateX(calc(-100% + $offset));
286300
video {
287301
display: none;
288302
}
@@ -292,14 +306,14 @@
292306
top: 0;
293307
right: 0;
294308
bottom: 0;
295-
left: 65px;
296-
width: calc(100% - 65px);
309+
left: $offset;
310+
width: calc(100% - $offset);
311+
}
312+
/* display menu toggle button far right */
313+
.header .menu,
314+
.header .menu li {
315+
margin-right: 2px;
297316
}
298-
}
299-
}
300-
@media only screen and (max-width: 768px) {
301-
#neko .neko-main .room-container {
302-
display: none;
303317
}
304318
}
305319
</style>
@@ -362,7 +376,7 @@
362376
})
363377
export default class extends Vue {
364378
@Ref('neko') readonly neko!: NekoCanvas
365-
expanded: boolean = true
379+
expanded: boolean = !window.matchMedia('(max-width: 600px)').matches // default to expanded on bigger screens
366380
loaded: boolean = false
367381
tab: string = ''
368382
@@ -371,6 +385,10 @@
371385
uploadActive = false
372386
uploadProgress = 0
373387
388+
get isTouchDevice(): boolean {
389+
return 'ontouchstart' in window || navigator.maxTouchPoints > 0
390+
}
391+
374392
dialogOverlayActive = false
375393
dialogRequestActive = false
376394
async dialogUploadFiles(files: File[]) {

0 commit comments

Comments
 (0)