diff --git a/.gitignore b/.gitignore index 1b1b0d1..8d5afe1 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,5 @@ dist # TernJS port file .tern-port +# Build files +_static/ diff --git a/v2/esp-app.ts b/v2/esp-app.ts index 90da69f..8c381c4 100644 --- a/v2/esp-app.ts +++ b/v2/esp-app.ts @@ -6,6 +6,7 @@ import "./esp-entity-table"; import "./esp-log"; import "./esp-switch"; import "./esp-logo"; +import "./esp-keyboard"; import cssReset from "./css/reset"; import cssButton from "./css/button"; @@ -102,6 +103,7 @@ export default class EspApp extends LitElement { ${this.config.title} +
diff --git a/v2/esp-entity-table.ts b/v2/esp-entity-table.ts index 795598a..f8fcde6 100644 --- a/v2/esp-entity-table.ts +++ b/v2/esp-entity-table.ts @@ -1,5 +1,5 @@ import { html, css, LitElement } from "lit"; -import { customElement, state } from "lit/decorators.js"; +import { customElement, state, property } from "lit/decorators.js"; import cssReset from "./css/reset"; import cssButton from "./css/button"; @@ -34,6 +34,8 @@ let basePath = getBasePath(); @customElement("esp-entity-table") export class EntityTable extends LitElement { @state({ type: Array, reflect: true }) entities: entityConfig[] = []; + @property() + keyboard_id = '' constructor() { super(); @@ -65,6 +67,9 @@ export class EntityTable extends LitElement { this.requestUpdate(); } }); + window.addEventListener('kbd-closed', (evt) => { + this.keyboard_id = '' + }); } actionButton(entity: entityConfig, label: String, action?: String) { let a = action || label.toLowerCase(); @@ -135,6 +140,12 @@ export class EntityTable extends LitElement { } control(entity: entityConfig) { + if (entity.domain === "keyboard") { + return html``; + } + if (entity.domain === "switch") return [this.switch(entity)]; if (entity.domain === "fan") { @@ -258,6 +269,15 @@ export class EntityTable extends LitElement { }); } + update_kbd_id(id: string) { + this.dispatchEvent(new CustomEvent('update-kbd-id', { + detail: id, + composed: true, + bubbles: true + })); + this.keyboard_id = id + } + render() { return html` diff --git a/v2/esp-keyboard-key.ts b/v2/esp-keyboard-key.ts new file mode 100644 index 0000000..df580fd --- /dev/null +++ b/v2/esp-keyboard-key.ts @@ -0,0 +1,176 @@ +import { LitElement, html, css, nothing } from "lit"; +import { customElement, property } from "lit/decorators.js"; + + +const pressedStyle = html` +`; + +@customElement("keyboard-key") +export default class EspKey extends LitElement { + @property() + modifier = false; + @property() + pressed = false; + @property() + code = '' + @property() + 'use-code'=false + + constructor() { + super(); + this.addEventListener("click", (evt) => { + this.togglePressedState(); + + // Don't send event if click removed "pressed" state + // on a modifier key. + if (!this.modifier || this.pressed) { + this.dispatchEvent( + new CustomEvent("keyclick", { + detail: { + key: this.key, + code: this.code, + isModifier: this.modifier, + }, + bubbles: true, + composed: true, + }) + ); + } else { + this.dispatchEvent( + new CustomEvent("keyrelease", { + detail: { + key: this.key, + code: this.code, + }, + bubbles: true, + composed: true, + }) + ); + } + }); + } + + get key() { + const bottomLabel = this.querySelector("[slot=bottom]") + + if (bottomLabel) { + return bottomLabel.innerHTML; + } + + if (this['use-code']) { + return this.code; + } + + const keyLabel = this.firstChild?.textContent; + // If the key is a single A-Z character, return the lowercase + // version. + if (keyLabel && /^[A-Z]$/.test(keyLabel)) { + return keyLabel.toLowerCase(); + } + return keyLabel; + } + + set press(newValue : boolean) { + // Only modifier keys can be "pressed". + if (this.modifier) { + this.pressed = newValue; + } + } + + togglePressedState() { + this.press = !this.pressed; + } + + render() { + return html` + ${this.pressed && pressedStyle || nothing } + `; + } + + static get styles() { + return [ + css` + .key { + position: relative; + display: block; + float: left; + width: 50px; + height: 50px; + font-size: 12px; + background-color: #fff; + line-height: 16px; + border-radius: 2px; + margin: 1px; + padding: 1px; + cursor: pointer; + border: 1px solid rgb(187, 187, 187); + text-align: left; + transition: background-color 0.25s; + } + + .key:hover { + background-color: #e3e3e3; + } + + .key:active { + background-color: #8f8f8f; + } + + .key .content-top { + position: absolute; + top: 5px; + left: 5px; + } + + .key .content-bottom { + position: absolute; + bottom: 5px; + left: 5px; + } + + :host(.hidden) .key { + visibility: hidden; + } + + :host(.accented) .key { + background-color: #dbdbdb; + } + + :host(.accented) .key:hover { + background-color: #b5b5b5; + } + + :host(.key-collapse-half) .key { + width: 24px; + } + + :host(.key-extend-half) .key { + width: 77px; + } + + :host(.key-extend-full) .key { + width: 102px; + } + + :host(.key-extend-full-half) .key { + width: 128px; + } + + :host(.key-space) .key { + width: 229px; + }`, + ] + } + +} diff --git a/v2/esp-keyboard.ts b/v2/esp-keyboard.ts new file mode 100644 index 0000000..f7480f2 --- /dev/null +++ b/v2/esp-keyboard.ts @@ -0,0 +1,485 @@ +import { LitElement, html, css } from "lit"; +import { customElement, property, queryAll, state } from "lit/decorators.js"; + +import "./esp-keyboard-key"; + +@customElement("esp-keyboard") +export default class EspKeyboard extends LitElement { + @queryAll("[modifier=true]") + _modifiers; + @property() + keyboard_id = '' + @state() + _socket : WebSocket = null + @state() + _opened = false + + connectedCallback() { + super.connectedCallback(); + + this.addEventListener("keyclick", (evt) => { + this.onKeyClick( + evt.detail.key, + evt.detail.code, + evt.detail.isModifier + ); + }); + + this.addEventListener("keyrelease", (evt) => { + this.emitKeyEvent("keyup", evt.detail.key, evt.detail.code); + }); + + window.addEventListener("update-kbd-id", (evt) => { + this._socket?.close(); + this._socket = null + this.keyboard_id = evt.detail + + if(this.keyboard_id != '') { + //TODO add ping ?? + let url = window.location.host + window.location.pathname; + url += url.endsWith("/") ? "" : "/"; + this._socket = new WebSocket('ws://' + url + 'keyboard/' + this.keyboard_id ); + this._socket.addEventListener('close', (event) => { + this.keyboard_id = '' + this._opened = false; + this._socket = null + this.dispatchEvent(new CustomEvent('kbd-closed', { + composed: true, + bubbles: true + })); + }); + + this._socket.addEventListener('error', (event) => { + this._socket.close() + }); + + this._socket.addEventListener('open', (event) => { + this._opened = true; + }); + } + }); + window.addEventListener("keyup", (evt) => { + this.sendKey(evt); + }); + window.addEventListener("keydown", (evt) => { + this.sendKey(evt); + }); + } + + sendKey(evt: KeyboardEvent) { + if(this._opened) { + evt.preventDefault(); + this._socket.send(evt.type.substring(3)[0] + evt.code); + } + } + + onKeyClick(key: string, code: string, isModifier : boolean) { + // Make uppercase when shift is active – but only if no other modifier + // is pressed at the same time. + if ( + ["ShiftLeft", "ShiftRight"].some((m) => + this.isModifierKeyPressed(m) + ) && + [ + "MetaLeft", + "MetaRight", + "AltLeft", + "AltRight", + "ControlLeft", + "ControlRight", + ].every((m) => !this.isModifierKeyPressed(m)) + ) { + key = this.getShiftedKeyValue(key); + } + + this.emitKeyEvent("keydown", key, code); + + if (!isModifier) { + this.emitKeyEvent("keyup", key, code); + + // Remove pressed state from all modifier keys if non-modifier key + // was pressed. + this.clearPressedKeys(); + } + } + + + isModifierKeyPressed(keyCode: string) { + let result = false; + this._modifiers.forEach((modifier) => { + if (modifier.pressed && modifier.code == keyCode) { + result = true; + return; + } + }); + return result; + } + + emitKeyEvent(eventType: string, key: string, code: string) { + this.dispatchEvent( + new KeyboardEvent(eventType, { + bubbles: true, + composed: true, + key: key, + code: code, + metaKey: + this.isModifierKeyPressed("MetaLeft") || + this.isModifierKeyPressed("MetaRight"), + ctrlKey: + this.isModifierKeyPressed("ControlLeft") || + this.isModifierKeyPressed("ControlRight"), + shiftKey: + this.isModifierKeyPressed("ShiftLeft") || + this.isModifierKeyPressed("ShiftRight"), + altKey: + this.isModifierKeyPressed("AltLeft") || + this.isModifierKeyPressed("AltRight"), + }) + ); + } + + clearPressedKeys() { + this._modifiers.forEach((modifier) => { + if (modifier.pressed) { + modifier.pressed = false; + this.emitKeyEvent("keyup", modifier.key, modifier.code); + } + }); + } + + getShiftedKeyValue(key) { + if (/^[a-z]$/.test(key)) { + return key.toUpperCase(); + } + const shiftMappings = { + "`": "~", + "1": "!", + "2": "@", + "3": "#", + "4": "$", + "5": "%", + "6": "^", + "7": "&", + "8": "*", + "9": "(", + "0": ")", + "-": "_", + "=": "+", + "[": "{", + "]": "}", + "\\": "|", + ";": ":", + "'": '"', + ",": "<", + ".": ">", + "/": "?", + }; + if (key in shiftMappings) { + return shiftMappings[key]; + } + // This key is the same regardless of whether the shift key is + // pressed. + return key; + } + + render() { + if(this['keyboard_id'] == '') { + return html`` + } + return html` +
+
+
+ Esc + + F1 + F2 + F3 + F4 + + + F5 + F6 + F7 + F8 + + + F9 + F10 + F11 + F12 +
+ + +
+ + ~ + \` + + + ! + 1 + + + @ + 2 + + + # + 3 + + + $ + 4 + + + % + 5 + + + ^ + 6 + + + & + 7 + + + * + 8 + + + ( + 9 + + + ) + 0 + + + _ + - + + + + + = + + + Backspace +
+ + +
+ Tab + Q + W + E + R + T + Y + U + I + O + P + + { + [ + + + } + ] + + + | + \ + +
+ +
+ Caps Lock + A + S + D + F + G + H + J + K + L + + : + ; + + + " + ' + + + Enter +
+ + +
+ Shift + + Z + X + C + V + B + N + M + + + , + + + > + . + + + ? + / + + Shift +
+ + +
+ Control + Meta + Alt + Space + Alt + Meta + Menu + Control +
+ +
+
+
+ Print + Scroll Lock + Pause +
+ +
+ Insert + Home + Page Up +
+ +
+ Delete + End + Page Down +
+ +
+ +
+ +
+ + Up + +
+ +
+ Left + Down + Right +
+ +
+
+ ` + } + + static get styles() { + return [ + css` + .keyboard { + width: 992px; + min-width: 992px; + background-color: #f7f7f7; + margin: 1rem auto; + padding: 14px; + color: #333; + border: 1px solid rgb(92, 92, 92); + } + + .keyboard-block { + display: inline-block; + } + + #keyboard-block-left { + margin-right: 14px; + width: 780px; + } + + #keyboard-block-right { + margin-left: 14px; + width: 156px; + } + + .keyboard-row { + width: 100%; + float: left; + } + + .keyboard-row-bump { + padding-bottom: 28px; + } + `, + ] + } +} diff --git a/vite.config.ts b/vite.config.ts index a57c808..e3f4838 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -93,6 +93,10 @@ export default defineConfig({ "/number": proxy_target, "/climate": proxy_target, "/events": proxy_target, + "/keyboard": { + target: proxy_target, + ws: true, + } }, }, });