<template> <div> <div v-if="!charTable">An error occured loading the keyboard</div> <div v-else v-for="(row, i) in charTable" :key="i"> <span v-for="(char, j) in row" :key="j"> <!-- Emit a key of the keyboard or its position when it is empty --> <button :class="{ 'is-selected': selectedKey && selectedKey.row == i && selectedKey.column == j, }" type="button" v-on:mouseup=" char ? $emit('input', char) : $emit('input', { row: i, column: j }) " > <template v-if="char">{{ char.character }}</template> <template v-else> </template> </button> </span> </div> <template v-if="resizable"> <label>Rows</label> <!-- Directly parse field value to handle integers only --> <input :value="rows" v-on:change.prevent="(e) => updateRows(e.target.value)" :min="rowsCount" :max="maxKeyboardSize" type="number" /> <label>Columns</label> <input :value="columns" v-on:change.prevent="(e) => updateColumns(e.target.value)" :min="columnsCount" :max="maxKeyboardSize" type="number" /> </template> </div> </template> <script> import { mapState } from "vuex"; import { maxKeyboardSize } from "../config.js"; export default { props: { keyboardIndex: { type: Number, required: true, }, // Row and column value of the selected keyboard key selectedKey: { type: Object, default: null, }, resizable: { type: Boolean, default: false, }, }, data: () => ({ // Rows and columns including a potential resize of the keyboard rows: 0, columns: 0, maxKeyboardSize, }), computed: { ...mapState(["keyboards"]), keyboard() { return this.keyboards[this.keyboardIndex]; }, characters() { return this.keyboard && this.keyboard.characters; }, rowsCount() { // Return the number of rows from keyboard characters if (!this.characters.length) return 1; return ( this.characters && Math.max(...this.characters.map((c) => c.row)) + 1 ); }, columnsCount() { // Return the number of columns from keyboard characters if (!this.characters.length) return 1; return ( this.characters && Math.max(...this.characters.map((c) => c.column)) + 1 ); }, charTable() { const [tableRows, tableCols] = this.resizable ? [this.rows, this.columns] : [this.rowsCount, this.columnsCount]; if (!tableRows || !tableCols) return; // Return a list of every column with padding const table = Array.from(Array(tableRows), () => new Array(tableCols)); if (!this.characters) return table; this.characters.forEach((c) => { table[c.row][c.column] = c; }); return table; }, }, methods: { // We need to handle input changes manually as v-model.number allows random numbers or empty strings updateRows(value) { if (isNaN(value) || value < this.rowsCount || value > maxKeyboardSize) return; this.rows = parseInt(value); }, updateColumns(value) { if (isNaN(value) || value < this.columnsCount || value > maxKeyboardSize) return; this.columns = parseInt(value); }, }, watch: { rowsCount: { immediate: true, handler(n) { if (!this.rows) this.rows = n || 1; }, }, columnsCount: { immediate: true, handler(n) { if (!this.columns) this.columns = n || 1; }, }, }, }; </script> <style scoped> button { width: 3rem; height: 3rem; margin: 0.1rem; } button.is-selected { border: solid black 0.25rem; border-radius: 0.25rem; } </style>