wrongwrongな開発日記

しんまいさんの忘備録

【HTML/CSS】複数の要素を同期してScrollする【JavaScript】

複数要素を同期してScrollさせます。
強引な実装だと思うので、もっとスマートな実装が有れば教えてください。

やりたいこと

以下のような機能を実現します。

  • 複数要素を同時にScrollさせる
  • 全ての要素から同様に操作が行えるようにする
  • 高さが異なる場合でも動く

実装

以下のようにすればやれます。

let scrollFrom = null; //呼び出し元記憶用
let timeoutId = 0; //タイムアウトの破棄用

//スクロール同期制御類
let handleScroll = (callFrom) => {
    //呼び出し元をセットする
    if(scrollFrom === null){
        scrollFrom = callFrom;
    }
    //呼び出し元が違うなら何もしない
    if(scrollFrom !== callFrom) return;

    //タイムアウトが設定されているなら破棄する
    if(timeoutId !== 0) clearTimeout(timeoutId);
    //タイムアウトを設定、timeoutと呼び出し元を初期化
    timeoutId = setTimeout(()=>{
        timeoutId = 0;
        scrollFrom = null;
    }, 100);

    //スクロール率を設定
    const basis = document.getElementById(callFrom);
    const scrollRatio = basis.scrollTop / (basis.scrollHeight - basis.clientHeight);
    //スクロール実行
    const elements = document.getElementsByClassName("sync-scroll");
    for(let i = 0; i < elements.length; i++){
        //呼び出し元は操作しない
        if(elements[i].id === callFrom) continue;
        //要素をscroll
        elements[i].scrollTop =
            (elements[i].scrollHeight - elements[i].clientHeight) * scrollRatio;
    }
}

利用例

動く例を貼ってみました。
解説用に開発者ツールへの出力を入れているので、解説を読む方はそちらも確認してみてください。



a

b

c

d

e

f

g

h

i

j

k

l

m

n

o

p


0

1

2

3

4

5

6

7

8

9

解説

コードを解説します。

呼び出し元の記録と、記録のリセット

このコードでは呼び出し元を記録したり、setTimeoutを利用してリセットしたりしています。
これは、onscrollは呼び出し元に関わらず実行されてしまうため、「要素1をスクロール→要素2がスクロール→要素1がスクロール……」と連鎖が起きるのを防ぐための仕組みです。
サンプルを動かした上で開発者ツールのconsoleログを見てもらえば分かりますが、handleScrollの呼び出しは1スクロールごとに2回起きています。
スクロールは短時間に連続して起こるものなので、一定時間だけ呼び出し元を記録し、その呼び出し元以外からの操作はブロックするという形で対応を行いました。
setTimeoutで作成したリセットタイマーはそのままにしておくと誤作動するため、clearTimeoutで破棄しています。

高さの操作

高さは$element.scrollTopに値を入れることで操作できます。
$element.scrollTopの範囲は$element.scrollHeight - $element.clientHeightで出せるので、後は全要素のスクロール率を揃えれば同期したスクロールが可能になります。