やりたいこと
- テキストエリア間のフォーカスを
KeyboardEvent
イベントで発火させて移動させたい - フォーカスをテキストエリアの最初とか途中とか任意の場所に移動させたい
- テキスト最後尾で↓キーを押したら次のテキストの先頭に移動させる
- テキストの先頭で↑キーを押したら前のテキストの最後尾に移動させる
結論
-
keydown
イベントで現在のテキストカーソルの位置を判定する -
keyup
イベントでフォーカスの移動、フォーカスの位置の指定を行う
See the Pen moving_focus by hodaka (@cornercrap) on CodePen.
動機
フォームなどのフォーカスの移動にはtab
キーを使うことが一般的かと思いますが、矢印キーで操作できた方が操作性も上がるかなと思い検証しました。しかし、思ったより難航したのでここに書き留めておきます。
主な登場人物
keydown/keyupイベント
それぞれキーを押したとき/離したときに発生するイベント
KeyboardEvent.code
押された/離したキーの種類
focus()
その要素がフォーカスされる
setSelectionRange(selectionStart, selectionEnd)
任意のテキストの開始と終了のインデックスを指定することでテキストを選択状態にする
→引数に同じインデックスを指定すればテキストカーソルの移動と同じ意味になる
今回操作するHTMLはこんな感じです。
<div>
<textarea id="textarea">テキストテキスト</textarea>
</div>
<div>
<textarea id="textarea2">テキスト2テキスト2</textarea>
</div>
失敗例1
まず手始めにkeyup
だけでできるだろうと思い、組んでみました。
const textArea = document.getElementById("textarea");
const textArea2 = document.getElementById("textarea2");
textArea.addEventListener("keyup", event => { // use keyup event
if(event.code == "ArrowDown" && textArea.selectionStart == textArea.value.length){
textArea2.focus();
}
});
textArea2.addEventListener("keyup", event => {
if(event.code == "ArrowUp" && textArea2.selectionStart == 0){
textArea.focus();
}
});
カーソルがテキストの途中にあってもフォーカスが移動してしまいます。
↓キーを押すとテキストカーソルは、複数行ある場合は次の行へ移動する、最終行だった場合、最後尾に移動
します。
最後尾に移動してしまうと後半の条件式が満たされてしまうのでフォーカスが移動してしまいます。
失敗例2
ならばとkeydown
で試してみます。テキストカーソルの位置が想定どおりに行かないのでsetSelectionRange()
で明示的に指定しています。
const textArea = document.getElementById("textarea");
const textArea2 = document.getElementById("textarea2");
textArea.addEventListener("keydown", event => { // use keydown event
if(event.code == "ArrowDown" && textArea.selectionStart == textArea.value.length){
textArea2.focus();
textArea2.setSelectionRange(0, 0);
}
});
textArea2.addEventListener("keydown", event => {
if(event.code == "ArrowUp" && textArea2.selectionStart == 0){
textArea.focus();
textArea.setSelectionRange(textArea.value.length, textArea.value.length);
}
});
setSelectionRange()
を使ってもカーソルの位置が指定できなくなってしまいました。
成功例?
keydown
の中ではsetSelectionRange()
がうまく機能していませんでした。
しかし、調べていたところ、setTimeout()を使って回避する方法が見つかりました。こちらを参考にしてみます。
const textArea = document.getElementById("textarea");
const textArea2 = document.getElementById("textarea2");
textArea.addEventListener("keydown", (event) => {
if (
event.code == "ArrowDown" &&
textArea.selectionStart == textArea.value.length
) {
window.setTimeout(() => { // use setTimeout()
textArea2.focus();
textArea2.setSelectionRange(0, 0);
});
}
});
textArea2.addEventListener("keydown", (event) => {
if (event.code == "ArrowUp" && textArea2.selectionStart == 0) {
window.setTimeout(() => {
textArea.focus();
textArea.setSelectionRange(textArea.value.length, textArea.value.length);
});
}
});
不思議なことに想定どおりに動いてくれています。とりあえず実装したい場合はこれでいいと思いますが、この解法に関しては最善策でないと述べられていますし、私自身動く理由がよくわからないので、もう少し模索してみます。
成功例(結論)
keyup
ではカーソルの位置が正しく判定されず、keydown
ではフォーカス後のカーソルがうまく指定できませんでした。
なので両方のイベントを使ってそれぞれの欠点を補います。
let movingFlg = false;
textArea.addEventListener("keydown", (event) => {
if (
event.code == "ArrowDown" &&
textArea.selectionStart == textArea.value.length
) {
movingFlg = true;
}
});
textArea.addEventListener("keyup", (event) => {
if (event.code == "ArrowDown" && movingFlg) {
movingFlg = false;
textArea2.focus();
textArea2.setSelectionRange(0, 0);
}
});
textArea2.addEventListener("keydown", (event) => {
if (event.code == "ArrowUp" && textArea2.selectionStart == 0) {
movingFlg = true;
}
});
textArea2.addEventListener("keyup", (event) => {
if (event.code == "ArrowUp" && movingFlg) {
movingFlg = false;
textArea.focus();
textArea.setSelectionRange(textArea.value.length, textArea.value.length);
}
});
keydown
では単に移動するべきかどうかを判定して、フォーカスやカーソル位置指定はkeyup
で行います。フラグも出現してコード量も多くなりましたが腑に落ちるレベルまでは到達できました。
終わりに
改善案・ご指摘・捕捉など歓迎です。優しく教えてください。