Files
multiplayer_crosswords/multiplayer_crosswords/webui/keyboard.js
2025-11-14 16:47:01 +01:00

156 lines
5.3 KiB
JavaScript

import { LitElement, html } from 'https://unpkg.com/lit@2.7.5/index.js?module';
/**
* <mobile-keyboard>
* A compact on-screen keyboard for mobile devices. Emits 'key-press' events on window
* with detail { type: 'letter'|'navigate'|'backspace', value }
*/
export class MobileKeyboard extends LitElement {
static properties = {
// reflect so CSS attribute selectors can react to collapsed state
collapsed: { type: Boolean, reflect: true },
_isMobile: { type: Boolean, state: true },
_wideScreen: { type: Boolean, state: true },
};
// styles moved to webui/styles.css; render into light DOM so external CSS applies
constructor() {
super();
this.collapsed = true; // default collapsed; _onResize will flip for mobile
this._isMobile = false;
this._letters = 'abcdefghijklmnopqrstuvwxyz'.split('');
this._onResize = this._onResize.bind(this);
}
createRenderRoot() { return this; }
render() {
// simple QWERTY-like rows
const rows = [
'qwertyuiop'.split(''),
'asdfghjkl'.split(''),
'zxcvbnm'.split(''),
];
// compute the maximum number of columns across rows (account for backspace in second row now)
const counts = rows.map((r, idx) => r.length + (idx === 1 ? 1 : 0));
const arrowCols = 3; // reserve 3 columns on the right for [left][down][right]
const baseMax = Math.max(...counts, 10);
const maxCols = baseMax;
return html`
<div class="keyboard-container">
${html`<div class="handle" @click=${this._toggleCollapse}>
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" style="vertical-align: middle; margin-right: 0.3rem;">
<path d="M20 5H4c-1.1 0-1.99.9-1.99 2L2 17c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-9 3h2v2h-2V8zm0 3h2v2h-2v-2zM8 8h2v2H8V8zm0 3h2v2H8v-2zm-1 2H5v-2h2v2zm0-3H5V8h2v2zm6 7H5v-2h8v2zm0-4h-2v-2h2v2zm3 0h-2v-2h2v2zm3 0h-2v-2h2v2zm0-3h-2V8h2v2z"/>
</svg>
${this.collapsed ? '▲' : '▼'}
</div>`}
<div class="keyboard-wrapper">
<div class="keyboard">
${rows.map((r, idx) => {
// center the letter keys leaving the rightmost `arrowCols` for the arrow block
let rowClasses = 'row';
if (idx === 1) rowClasses += ' stagger'; // A row
if (idx === 2) rowClasses += ' stagger-deep'; // Z row needs a larger indent
return html`<div class="${rowClasses}" style="--cols:${maxCols-idx}; --arrow-cols:${arrowCols};">
<div class="keys">
${r.map(l => html`<button @click=${() => this._emitLetter(l)}>${l}</button>`) }
${idx === 1 ? html`<button class="backspace" @click=${() => this._emitBackspace()}>⌫</button>` : ''}
</div>
<div class="arrows">
${Array.from({ length: arrowCols }).map((_, i) => {
if (idx === 2 && i === 1) return html`<button class="nav" @click=${() => this._emitNavigate('up')}>▲</button>`;
return html`<div class="key-spacer"></div>`;
})}
</div>
</div>`;
})}
<!-- spacebar row -->
<div class="row" style="--cols:${maxCols};">
<!-- spacebar spans all but the right arrow columns -->
<button class="space" @click=${() => this._emitSpace()}></button>
<!-- arrow columns: left, down, right (will occupy the last 3 columns) -->
<button class="nav" @click=${() => this._emitNavigate('left')}></button>
<button class="nav" @click=${() => this._emitNavigate('down')}></button>
<button class="nav" @click=${() => this._emitNavigate('right')}></button>
</div>
</div>
</div>
</div>
`;
} _emitLetter(l) {
this._vibrate();
this._emit({ type: 'letter', value: l });
}
_emitNavigate(dir) {
this._vibrate();
this._emit({ type: 'navigate', value: dir });
}
_emitBackspace() {
this._vibrate();
this._emit({ type: 'backspace' });
}
_emitSpace() {
this._vibrate();
this._emit({ type: 'letter', value: '' });
}
_emit(detail) {
window.dispatchEvent(new CustomEvent('key-press', { detail }));
}
_vibrate() {
// Use Vibration API for haptic feedback
try {
console.log('Attempting vibration... navigator.vibrate:', typeof navigator.vibrate);
if (navigator.vibrate) {
navigator.vibrate(10); // 10ms short buzz
console.log('Vibration sent!');
} else {
console.log('Vibration API not available on this device');
}
} catch (e) {
console.warn('Vibration API error:', e);
}
}
connectedCallback() {
super.connectedCallback();
window.addEventListener('resize', this._onResize);
this._onResize();
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener('resize', this._onResize);
}
_onResize() {
const mobile = window.innerWidth <= 900;
this._isMobile = mobile;
this.classList.toggle('mobile', mobile);
this.classList.toggle('desktop', !mobile);
// decide wide-screen (landscape/tablet) to change layout behavior
const wide = (window.innerWidth / window.innerHeight) > 1.6;
this._wideScreen = wide;
this.classList.toggle('wide-screen', wide);
// collapsed default: expanded on mobile, collapsed on desktop
if (mobile) this.collapsed = false;
else this.collapsed = true;
}
_toggleCollapse() {
this.collapsed = !this.collapsed;
if (this.collapsed) this.setAttribute('collapsed', '');
else this.removeAttribute('collapsed');
}
}
customElements.define('mobile-keyboard', MobileKeyboard);