<template> <div class="virtual-keyboard"> <!-- Allow focus on the keyboard to automatically restore focus on the input --> <section tabindex="0"> <div class="header"> <a v-if="manager" v-on:click.prevent="optionsModal = true" href="#"> Manage Keyboards </a> </div> <div class="content"> <KeyboardDisplay :keyboard="keyboard" v-on:input="addChar" /> </div> <Modal v-if="optionsModal" v-model="optionsModal"> <KeyboardManager /> </Modal> </section> </div> </template> <script> import { mapState } from "vuex"; import KeyboardDisplay from "./KeyboardDisplay.vue"; import KeyboardManager from "./KeyboardManager.vue"; import Modal from "./Modal.vue"; export default { components: { KeyboardDisplay, KeyboardManager, Modal, }, props: { inputField: { type: [HTMLInputElement, HTMLTextAreaElement], required: true, }, manager: { type: Boolean, default: false, }, }, data: () => ({ keysSwitch: { a: "ǻ", A: "Ǡ", }, optionsModal: false, selectedKeyboard: null, }), created() { this.inputField.addEventListener("blur", this.looseFocus); this.inputField.addEventListener("keydown", this.keyInput); }, beforeDestroy() { this.inputField.removeEventListener("blur", this.looseFocus); this.inputField.removeEventListener("keydown", this.keyInput); }, computed: { ...mapState(["keyboards"]), keyboard() { if (!this.selectedKeyboard) return; return this.keyboards[this.selectedKeyboard]; }, }, methods: { selectDefaultKeyboard() { const keyboard = Object.values(this.keyboards).find( (k) => (k.default = true) ); if (keyboard) this.selectedKeyboard = keyboard.name; else if (this.keyboards.length) this.keyboards[0].name; }, addChar(key) { if (!key.character) return; // Add a character to the input depending on the cursor selection const start = this.inputField.selectionStart; const value = this.inputField.value; this.inputField.value = value.slice(0, start) + key.character + value.slice(this.inputField.selectionEnd); // Reset the input caret after modifying the input value this.inputField.selectionStart = this.inputField.selectionEnd = start + key.character.length; }, keyInput(e) { const character = this.keysSwitch[e.key]; if (!character) return; this.addChar({ character }); e.preventDefault(); }, looseFocus() { if (this.optionsModal) return; // We need a short delay before checking where the user moved the focus setTimeout(() => { // In case the user typed on the keyboard, restore focus on the input if (this.$el.contains(document.activeElement)) return this.inputField.focus(); // Otherwise emit a custom event on the input for the keyboard to be removed this.inputField.dispatchEvent( new CustomEvent("vk_rmkeyboard", { detail: this }) ); }, 1); }, }, watch: { optionsModal(value) { // Focus the input when closing the modal if (!value) this.inputField.focus(); }, keyboards: { immediate: true, handler() { if (!this.selectedKeyboard || !this.keyboards[this.selectedKeyboard]) { this.selectDefaultKeyboard(); } }, }, }, }; </script> <style scoped> .virtual-keyboard { /* Avoid properties inheritance */ all: initial; /* Override required attributes */ position: absolute; display: flex; /* Arbitrary z-index */ z-index: 99999; min-width: 20rem; } .virtual-keyboard > section { /* reset style in order to use Pure CSS framework only */ all: unset; width: 100%; background-color: white; border: solid #bfbfbf 1px; border-radius: 0.25rem; } .virtual-keyboard .header { border-bottom: solid #bfbfbf 1px; height: 2rem; } .virtual-keyboard .content { padding: 0.25rem; } </style>