[Javascript] 2つ以上のリストボックスを相互に同期スクロールする方法

2021年8月21日

Javascript テクノロジー

同期スクロールに、悶え苦しんでしまった、ユゲタです。 同期スクロールって何かというと、 2つの関連したリストを表示するような場合に、それぞれリストがスクロールするほど長い状態の時、 その2つのリストの片方をスクロールしたら、もう片方がそれを追従するようにリストをスクロールさせたい場合に、 同期スクロールという方法をとるようにする事なんですが、 javascriptで、リストのスクロールイベントをキャッチして、もう片方のリストのスクロール値を変更してあげればいいだけなんですが、 思いもよらず、うまく動かなかったので、その苦労の状態を備忘録しておきたいと思います。

相互の動機スクロールでの問題点

とりあえず、まずは最初にコードを書いてみます。 <link rel="stylesheet" href="common.css"> <div class="flex"> <ul id="a"> <li>1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> <li>6</li> <li>7</li> <li>8</li> <li>9</li> <li>10</li> <li>11</li> <li>12</li> <li>13</li> <li>14</li> <li>15</li> <li>16</li> <li>17</li> <li>18</li> <li>19</li> <li>20</li> </ul> <ul id="b"> <li>a</li> <li>b</li> <li>c</li> <li>d</li> <li>e</li> <li>f</li> <li>g</li> <li>h</li> <li>i</li> <li>j</li> <li>k</li> <li>l</li> <li>m</li> <li>n</li> <li>o</li> <li>p</li> <li>q</li> <li>r</li> <li>s</li> <li>t</li> </ul> </div> <script src="scroll_sync.js"></script> ul,li{ list-style:none; margin:0; padding:0; } ul{ margin:10px; overflow:scroll; width:300px; height:300px; border:1px solid black; } li{ padding:5px; border-bottom:1px solid #ccc; } .flex{ display:flex; } document.getElementById("a").addEventListener("scroll" , scroll_sync); function scroll_sync(e){ let target = null; switch(e.target.id){ case "a": target = document.getElementById("b"); break; } target.scrollTop = e.target.scrollTop; } これを実行すると、左側のスクロールをした場合に、右側が一緒にスクロールします。 そして、今度は、右側のスクロールをした時に、左側のスクロールをさせたい場合に、 効率を考えて、同じscroll_sym関数を利用するために、次のコードをjsに付け足してみました。 document.getElementById("b").addEventListener("scroll" , scroll_sync); サンプルの画面では、ぎこちなくスクロール同期されていますが、少しemenentが増えてくると 片方がうまくスクロールされなくなります。 これは、対象のスクロールイベントが発火してしまって、相互にスクロール同期をやり合う、コンフリクトに近い状態になているようです。

回避方法は力技

とにかくぎこちなさをなくすために、今回とった策として、次のようなjsコードにしてみました。 window.a = document.getElementById("a"); window.b = document.getElementById("b"); a.addEventListener("scroll" , scroll_sync); b.addEventListener("scroll" , scroll_sync); function scroll_sync(e){ let target = null; if(window.scroll_sync_target && window.scroll_sync_target !== e.target){return;} switch(e.target.id){ case "a": target = window.b; break; case "b": target = window.a; break; } window.scroll_sync_target = e.target; target.scrollTop = e.target.scrollTop; setTimeout((function(){if(window.scroll_sync_target){delete window.scroll_sync_target}}) , 300); } 最初に発火したタイミングで、window.scroll_sync_targetに、発火したエレメントを保持しておき、 0.3秒後に、そのフラグを削除しています。(もう少し短い秒数でもいいですが) そして、対象エレメントのイベント発火の際に、window.scroll_sync_targetと同じエレメントでなければ、 returnして、処理をしないようにしています。 あと、毎回getElementをしている非効率を、グローバル変数で、最初一回のみの処理にすることで、 ほんのちょっぴり効率を良くしています。 でも、そもそもの、scrollイベントのタイムラグがあるため、多少のぎこちなさは残りますし、 めちゃくちゃ素早くスクロールすると、同期されない場合もあります。 この辺は、他のイベントと組み合わせて行うしかないでしょうね。 とりあえず、今回は2つのスクロール同期をやってみましたが、Googleのスプレッドシートのフレームのように、縦横でのスクロール同期などを やる場合もあるので、この辺の精度の良さ追求しておきたいですね。 Googleもちょびっとぎこちないんで、仕方ないかもしれませんが・・・

デモ

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • a
  • b
  • c
  • d
  • e
  • f
  • g
  • h
  • i
  • j
  • k
  • l
  • m
  • n
  • o
  • p
  • q
  • r
  • s
  • t