JavaScriptで任意の要素の座標取得/要素移動/スクロール
「JavaScriptでのページ内スクロールとDOCTYPE宣言でハマった話」の続きみたいなものですが、今回はページ最上部/最下部へのスクロールではなく、任意の要素位置へのスクロールについてサンプルを交えて語ってみたいと思います。
wikiでよくあるラジオボタン選択式のコメント欄
要素の座標取得やスクロール方法について解説する前に、なぜそんなことが必要になるのか、について軽く触れます。
ゲーム系の攻略wikiを利用したことがある方なら見慣れたUIだと思いますが、ページ最下部にコメント欄が配置されており、そのコメントへのコメント(いわゆるリプライ)をする際に、ラジオボタンで返信先を選択してからコメントを書くようなUIです。
本来、wikiでページ内にコメントを付ける場合、ほんの一言二言だったり、最新のコメントに対する返信をする程度なので、このUIでもあまり不便は感じないかも知れませんが、質問掲示板や雑談掲示板などを実装する際、このコメントシステムをそのまま採用してしまって使いづらくなっているケースがあります。
質問掲示板などの場合、文章量が多くなる傾向がありますし、いくつかレスが進んだ後に、過去のコメントにリプライするとなると、返信先とコメント欄が離れていて使いづらいと感じることはないでしょうか?
この場合、一番上のコメントを読みながらコメント欄に文字を入力するのはかなりしんどいと思います。
PCで表示している場合、上の例は大げさに思えるかも知れませんがPC/スマホ両対応のHPが当たり前の昨今、PCでは問題なかったUIもスマホだと文章がたくさん折り返されて使いづらくなってしまうケースが増えています。
動的にテキストボックスを移動してしまおう
実行例
- ここをクリックすると直下にテキストボックスが現れます
- ここをクリックすると直下にテキストボックスが現れます
-
こんなふうに途中に文章量が多いコメントがあってもテキストボックスが返信先の位置に現れるので問題ない
- ここをクリックすると直下にテキストボックスが現れます
- ここをクリックすると直下にテキストボックスが現れます
どうでしょう。このUIなら文章を読みながらの返信もやりやすいのではないでしょうか。
単にテキストボックスを任意の要素(この例ではLI要素)の下に移動するにはappendChildを使うだけなのですが、縦長になってしまい、コメント欄が画面外に表示されてしまった場合を想定すると、要素位置へのスクロールも必要となってきます。
HTMLソース
<ul class="bbs_tree">
<li>ここをクリックすると直下にテキストボックスが現れます
<div id="bbs_reply"><textarea id="bbs_reply" placeholder="コメント"></textarea></div>
</li>
<li>ここをクリックすると直下にテキストボックスが現れます</li>
<li>
<p style="width:1em;">
こんなふうに途中に文章量が多いコメントがあってもテキストボックスが返信先の位置に現れるので問題ない
</p>
</li>
<li>ここをクリックすると直下にテキストボックスが現れます</li>
<li>ここをクリックすると直下にテキストボックスが現れます</li>
</ul>
リストはUL/LIで書き、LI要素の好きな位置にDIV要素(id="bbs_reply")で囲んだテキストエリアを配置しています。
CSSソース
<style>
.bbs_tree {
border-radius:6px;
border:1px solid #cccccc;
padding:1em 1em 1em 2em;
max-width:300px;
}
.bbs_tree li {
cursor:pointer;
margin-bottom:2em;
}
#bbs_reply::before {
content:'';
border-left:1px dotted black;
border-bottom:1px dotted black;
width:10px;
height:10px;
position:absolute;
top:0;
left:0;
}
#bbs_reply {
position:relative;
display:block;
max-width:300px;
margin-left:10px;
box-sizing:border-box;
}
#bbs_reply textarea {
width:100%;
}
</style>
CSSではコメント欄を角丸で囲ったり、クリックできるとわかるようにマウスカーソルをpointerにしたり、と見た目をいじっているだけで動作関係(hover等)のギミックは使っていません。
少し特殊な部分としては、リプライっぽくなるように #bbs_reply::before 要素で、罫線(┗)を描いているくらいですかね。
::beforeは疑似要素で、その要素の直前に何かを挿入したり装飾するのに使います。この例ではテキストエリアの手前に線を描画したかったので追加していますが、わざわざDIVで囲っているのはTEXTAREAやINPUT等の入力パーツには疑似要素を付けられないため、です。
JavaScriptソース
<script>
window.addEventListener('load', function () {
document.querySelectorAll('.bbs_tree li').forEach(elm => {
elm.addEventListener('click', function () {
let elm_reply = document.getElementById('bbs_reply');
elm.appendChild(elm_reply); //LI要素の下に移動
let rect = elm_reply.getBoundingClientRect();
if (rect.top >= document.documentElement.clientHeight) {
window.scrollBy(0, rect.top - document.documentElement.clientHeight + rect.height);
}
});
});
document.getElementById('bbs_reply').addEventListener('click', function () {
event.stopPropagation();
});
});
</script>
どうでしょう。案外短くないですか?
テキストエリアの位置変更に至っては↓コレだけです。
- .bbs_tree li にonclickイベントを追加
- そのイベント内ではid=bbs_replyの要素を、クリックされたLI要素の下に移動(appendChild)
- テキストエリアのクリック時にLI要素のイベントまで発火してしまうため、event.stopPropagation();でイベントを止める
基本コレだけでテキストエリアの移動という目標は達成しているのですが、クリック時にテキストエリアが画面外にあった場合、ページに変化が現れないのでユーザーにはわかりづらいですよね?
返信しようと思ったのにページでは何も起こらない………ように見える。
そのため、
- LI要素の下に移動したテキストエリアの座標を取得
getBoundingClientRectの部分 - テキストエリアが画面外かどうか確認
rect.top >= document.documentElement.clientHeightの部分 - 画面外だった場合はテキストエリアのあたりまでスクロール
window.scrollByの部分
という処理を追加した感じです。
注意点としては、getBoundingClientRectで取得できる座標はページ内の絶対座標ではなく相対座標である点。(=現在のスクロール位置からの座標)
そして前回の「JavaScriptでのページ内スクロールとDOCTYPE宣言でハマった話」で書いたとおり、documentElement.clientHeightはブラウザで現在見えている範囲内の高さなので、この値よりもrect.topが大きければ入力欄が画面外にあるな、と判断できます。
そして、windows.scrollToを使ってスクロール……………………でも良かったのですが、scrollToは絶対座標指定のため、相対座標を絶対座標へ変換してやる必要があります。
これが面倒…というわけでもないのですが、window.pageYOffsetを足すだけだし…座標の取得も、位置の把握も相対座標で計算しているのだから、スクロールの移動量も相対指定で良くね?と思い、scrollByのほうを使っています。
↓この部分。
window.scrollBy(0, rect.top - document.documentElement.clientHeight + rect.height);
テキストエリアのY座標(rect.top)から、現在ブラウザで表示している高さを引けばテキストエリアの上辺のY座標が取得できます。そこにテキストエリアの高さ(rect.height)を加算したのが相対スクロール量、というわけです。
実際に使ってみるとページ最下部にぴったりテキストエリアが来るのはやや窮屈に感じるため、スクロール量を+100~200くらいしてテキストエリアの位置を少しばかり持ち上げると入力しやすいです。
まとめ
前回の「JavaScriptでのページ内スクロールとDOCTYPE宣言でハマった話」に引き続き、今度は任意の要素の座標を取得して、その位置へスクロールする例を取り上げてみました。
毎度思うんですが、いちいち長い説明書かんでもソース見ればわかるよね…。
ただ、前回のDOCTYPEの例のように、サンプルコードをコピペしても動かん!などとハマってしまったとき、もしかしたら、も~し~か~し~た~ら~、万に一つでも長たらしい説明文が役に立つかも知れない、と考えるとついつい文章量が多くなってしまいます。
今回は掲示板のようなツリー形式がわかりやすいかなーと思って例題に出しましたが、実際ぼくが使うのはECサイトの管理画面などで商品分類別のツリーを表示し、任意の位置に新しい商品を挿入するようなUIですね。
簡単なJavaScriptとCSSで横並びプルダウンを自作する で書いたとおり、ツリー形式での一覧性の良さを求められるケースは多く、入力作業の効率化を求められた場合、今回のような動的な要素移動と共にUIに組み込むこともあります。
それでは、今回は以上ですが、ここに書いたコードや解説がどこかの誰かのお役に立てたなら幸いです。いつもどおり、ソースコードは(大したものじゃないので…)お好きにお使いください。