Crowi の実装メモには、まだ実装できてないけど「やる」と宣言されているものがある。その中をつらつら見ていて「プレビュースクロールのsync」というものを見つけた。これは、Qiita などで実装されているもので、エディタ部のスクロールに合わせてプレビューもスクロールするという機能のことだ(と思う)。
どうやれば実現できるかなー?とふわふわ考えていたら、できそうな気配を感じたので、実装してみた。でも、この考えで良いのかどうかがいまひとつ自信がないのと、後で自分でも忘れそうなのでメモ代わりに日記に書いておく。
Crowi で編集する時には、画面左側に <textarea>
があり、そこで Markdown を入力するようになっている。そして画面右側には、その Markdown をレンダリングした結果の HTML が表示される。
ここで Markdown のテキストが長くなってくると、画面およびコンテンツは次のような状態になる。コンテンツは、画面には見えないところまで伸びており、エディタ部・プレビュー部ともにスクロールバーが表示される。
この状態で、エディタ部のスクロールバーを動かすと、連動してプレビュー部もスクロールするというのが、この機能の概要である。
「エディタ部のスクロール率とプレビュー部のスクロール率を一致させる」という動きにしたらどうなのか?ということを考えてみた。
つまり、エディタ部がスクロール率 0% ならプレビュー部も 0%、エディタ部が 100% ならプレビュー部も 100% になるようにする。0% というのは、コンテンツの一番上が表示されている状態、100% は一番下が表示されている状態だとする。
同期した動きは、scroll イベントで実現できそうに思った。
JavaScript で「スクロール率」を一発で取得する方法は無い。いや、実はあるのかも知れないけど、少なくともオレは知らないので、どうにかして算出する方法を考えた。
スクロールに応じて変動する値として scrollTop
があった。具体的には document.querySelector('#edit-form').scrollTop
という値である。これは 0 からスタートして、下にスクロールしていくごとに値が増えていき、最下部までスクロールするとある数字で固定値になる。
つまり、scrollTop == 0
の時はスクロール率 0% で、scrollTop
が最大値になったらスクロール率 100% とみなせるのではないかと考えた。
スクロール率 | scrollTop の様子 |
---|---|
0 % | |
nn % | |
100 % |
では、どうやれば「scrollTop の最大値」が取得できるのか?
そこで出てくるのが scrollHeight
と getBoundingClientRect()
である。先ほどの scrollTop
を含めて、それぞれ次のように取得できる。
var editor = document.querySelector('#edit-form');
var scrollTop = editor.scrollTop;
var scrollHeight = editor.scrollHeight;
var rect = editor.getBoundingClientRect();
rect
はオブジェクトが返ってくる。そのキーには、top
, bottom
, width
, height
などがある。また、scrollTop
と違って、scrollHeight
と rect
の値はスクロールしても変化しない。
そして、これらの数字の関係は次のようになる。
スクロール率 | 各数字の関係 |
---|---|
0% | |
100% |
この関係から分かるように、scrollTop
の最大値は次の式で求められる。
var maxScrollTop = scrollHeight - rect.height;
最大値が取得できれば、この数字を100%として「スクロール率」の計算ができる。ここでは rate
と呼ぶことにする。後で計算に使うので、パーセント数を出すための 100 倍はしない。
var rate = scrollTop / maxScrollTop;
同様にプレビュー部の scrollTop
の最大値を取得する。取得方法は、エディタ部と同じである。ここでは説明を簡略化するために、エディタ部での計算時と変数名を同じものにしている。
var preview = document.querySelector('#preview-body');
var scrollTop = preview.scrollTop;
var scrollHeight = preview.scrollHeight;
var rect = preview.getBoundingClientRect();
var maxScrollTop = scrollHeight - rect.height;
このプレビュー部の maxScrollTop
に、エディタ部の rate
をかけることにより、「エディタ部のスクロール率とプレビュー部のスクロール率を一致させる」ということになる。
上記の考えを実装してみたのがこの pull request になる。
実際に動かすと、このような挙動になる。
この動作は「スクロール率を合わせる」というもので、「エディタ側で見えているライン」と「プレビュー側で見えているライン」が必ずしも一致するものではない。Markdown の内容やプレビューのレンダリング結果によっては、位置がズレることがある。したがって、これを「同期」というと少し言い過ぎな気がする。
実際の動作を言葉にしてみると、「このあたりに書かれているものは、なんとなくこのあたりに書かれているだろうから、そこを表示する」みたいなことになる。
それでも、この実装での動作をみると Qiita でのスクロール同期と比較してもあまり違和感がないので、そんなに悪くないかなと思っている。
ただ、もしかすると「スクロール位置同期ができないなら見出し同期にすればいいじゃない」に書かれているように、コンテンツの見出し位置を同期したほうが、書いている人には嬉しいのかもしれない。
あ、念のため言っておくと、Qiita のコードは見ていない。ブラウザ上でのスクロール動作は参考にしたけど。
【追記】pull request はマージされた。