雑文発散

«前の日記(2016-06-12) 最新 次の日記(2016-06-14)» 編集
過去の日記

2016-06-13 [長年日記]

[Crowi] Crowi のエディタとプレビューのスクロールを同期させてみる

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 の最大値」が取得できるのか?

そこで出てくるのが scrollHeightgetBoundingClientRect() である。先ほどの 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 と違って、scrollHeightrect の値はスクロールしても変化しない。

そして、これらの数字の関係は次のようになる。

スクロール率 各数字の関係
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 はマージされた。

[] 第96回 朝活を実施した

今朝の活動報告。

  • Crowi いじり
  • Crowi ネタの日記書き

ひさびさにエンジニアっぽい(?)日記を書いた。