typeof Diary

VimとかJSとか。やったことのメモ。自分のため。

スクロールを連動させる

Qiitaなんかである入力エリアとプレビューエリアのスクロールが連動するやつ。
要件は違ったんですが、スクロール同期という点では同じだったのでやってみたので書いておきます。

単純に連動する

まずは単純に同じサイズの要素を連動させてみます。
単純な連動デモ

const areaA = document.getElementById('area-a')
const areaB = document.getElementById('area-b')

areaA.addEventListener('scroll', e => {
  const scrollTop = e.target.scrollTop
  areaB.scrollTo(0, scrollTop)
})

同じサイズのA→Bと連動させるなら、AのスクロールイベントでscrollTopをとって、BのscrollTo()に放り込んであげるだけです。  

サイズが違う要素を連動する

次にQiitaみたいな要素の大きさが異なる場合の連動を見ていきます。
サイズ違いの連動デモ

const areaA = document.getElementById('area-a')
const areaB = document.getElementById('area-b')

areaA.addEventListener('scroll', e => {
  const scrollMaxA = e.target.scrollHeight - e.target.clientHeight
  const scrollMaxB = areaB.scrollHeight - areaB.clientHeight
  const percent = e.target.scrollTop / scrollMaxA

  areaB.scrollTo(0, scrollMaxB * percent)
})

さっきと異なるのがサイズが異なるので、何%スクロールしたのかを求めることが必要です。
まずはscrollHeight - clientHeightで、どれだけスクロールできるのかをそれぞれ求めます。
次にAのscrollTop / scrollMaxAで%を計算。
最後にBのscrollToscrollMaxB * percent)でスクロール量を放り込んであげて完成です!

相互連動させる

最後に相互に連動させる必要がある場合です。
相互連動デモ

const areaA = document.getElementById('area-a')
const areaB = document.getElementById('area-b')

function scrollA(e) {
  const scrollTop = e.target.scrollTop
  areaB.scrollTo(0, scrollTop)
}

function scrollB(e) {
  const scrollTop = e.target.scrollTop
  areaA.scrollTo(0, scrollTop)
}

areaA.addEventListener('scroll', scrollA)
areaB.addEventListener('scroll', scrollB)

areaA.addEventListener('mouseenter', e => {
  areaB.removeEventListener('scroll', scrollB)
})
areaB.addEventListener('mouseenter', e => {
  areaA.removeEventListener('scroll', scrollA)
})

areaA.addEventListener('mouseleave', e => {
  areaB.addEventListener('scroll', scrollB)
})
areaB.addEventListener('mouseleave', e => {
  areaA.addEventListener('scroll', scrollA)
})

ちょっと面倒な書き方してるかもしれないですが。。。
相互連動するとA→Bをしたときに、Bのスクロールイベントがトリガーされます。
デモぐらい単純な中身なら気にはなりませんが、たまーにズレたりします。

そこで、AをスクロールさせたときはBのイベントをトリガーしない。
BをスクロールしたときはAのイベントをトリガーしない。みたいなことが必要になります。

方法は

  • どこからスクロールされているのかを明確にする
  • そもそもイベント自体を消してしまう

どっちかだと思いますが、今回は後者です。

各要素にmouseenterしたら別要素のイベントをremoveEventListenerで削除。
mouseleaveしたら別要素にイベントをaddEventListenerで付与しています。