input要素のcaretも自作したい
ターミナルみたいなinput要素を作りたい
標準のCSSだとcaretのstyleを当てれないのでJSで色々しないといけない
色々あってElmで使いたいのでUIの制御はJSに任せたい
そこでWebComponentsを使う
利用するライブラリはlit-element
<terminal-input></terminal-input>
みたいな感じで書けるようにする
早速つくる
import { LitElement, html, css, property } from 'lit-element'
export class TerminalInput extends LitElement {
@property({type: String})
value: string = ""
constructor() {
super()
}
focus() {
this.shadowRoot?.querySelector('input')?.focus()
}
static get styles() {
return css`
:host {
display: inline;
}
div {
display: inline;
}
`
}
render() {
return html`
<div>
<input
.value="${this.value}"
@input="${this.bindValue}"
>
</div>
`
}
private bindValue(e: InputEvent) {
this.value = (e.target as HTMLInputElement | null)?.value ?? ""
}
}
詰まったポイント
value
の扱い
@property({type: String}) value: string = ""
の部分でこのCustomElementのフィールドとしての value
と属性としての value
が紐付けられるのだが ShadowDOM内にある input
の value
とは紐付けられないので自分でbindしてあげる必要がある。
やっている部分は bindValue
メソッド
import { LitElement, html, css, property } from 'lit-element'
export class TerminalInput extends LitElement {
@property({type: String})
value: string = ""
@property({type: Number})
selectionStart: number = 0
@property({type: Number})
selectionEnd: number = 0
constructor() {
super()
}
focus() {
this.shadowRoot?.querySelector('input')?.focus()
}
static get styles() {
return css`
:host {
display: inline;
}
div {
display: inline;
}
input {
position: absolute;
opacity: 0;
}
.caret {
background: green;
white-space: pre;
}
`
}
render() {
return html`
<div>
<input
.value="${this.value}"
@input="${this.bindValue}"
@keydown="${this.caretUpdate}"
@select="${this.caretUpdate}"
>
<span>
${this.beforeString}<span class="caret">${this.currentChar}</span>${this.afterString}
</span>
</div>
`
}
private caretUpdate(e: KeyboardEvent | InputEvent) {
const input = e.target as HTMLInputElement;
this.selectionStart = input.selectionStart ?? 0
this.selectionEnd = input.selectionEnd ?? 0
}
private bindValue(e: InputEvent) {
this.value = (e.target as HTMLInputElement | null)?.value ?? ""
this.caretUpdate(e)
}
private get beforeString() {
return this.value.slice(0, this.selectionStart)
}
private get currentChar() {
return this.value.slice(this.selectionStart, this.selectionEnd) || " "
}
private get afterString() {
return this.value.slice(this.selectionEnd)
}
}
input
の部分は全部自分でstyle等を整えるので実際の input
要素は KeyboardEvent を受け取る君として振る舞ってもらい普段は見えないようにしておく。
勝手にlayoutされると面倒なので position: absolute
で適当なところに置いておく
カーソルの位置を保存しておかないといけないので selection(Start|End)
をプロパティに作る
- keydown
- input
- select
イベントのタイミングで更新するようにする
keydownでselectionStartを取得してると矢印キーを押したときに
keydown event → caretの移動
なのでselectionStartがズレる、どうしようか
private caretUpdate(e: KeyboardEvent | InputEvent) {
const input = e.target as HTMLInputElement;
if (e instanceof KeyboardEvent) {
if ((e as KeyboardEvent).key === 'ArrowRight') {
this.selectionStart = Math.min(this.selectionStart + 1, input.value.length)
this.selectionEnd = Math.min(this.selectionEnd + 1, input.value.length)
} else if ((e as KeyboardEvent).key === 'ArrowLeft') {
this.selectionStart = Math.max(this.selectionStart - 1, 0)
this.selectionEnd = Math.max(this.selectionEnd - 1, 0)
}
} else {
this.selectionStart = input.selectionStart ?? 0
this.selectionEnd = input.selectionEnd ?? 0
}
}
KeyboardEvent
だったら
結構ゴリ押し感がある
←を押したら今の値から -1
→を押したら今の値から +1
それ以外だったら特に反応しない
InputEvent
だったら
inputの selection(Start|End)
を使う
完成したものがこちら