DocBaseのスクロール同期を実現しているアルゴリズム このエントリをはてなブックマークに登録

2021年07月06日

takurutakuru

皆さんこんにちは、KRAYの浅海です。

今回は、DocBaseのメモ編集画面にて、エディタ画面とプレビュー画面のスクロールをいかにして同期させているかという話をしたいと思います。

DocBaseにおけるスクロールの同期とは

エディタ部分とプレビュー画面のスクロールの同期とはどういうものなのかを説明するために、画面のキャプチャを用意しました。

画面の左がMarkdownを編集しているエディタ、右がそのプレビューです。この映像では左のエディタ画面をスクロールしているのですが、それに対応して右の画面もスクロールされています。
プレビュー画面のスクロールの速度に注目してみてください。画像の部分ではスクロール速度が速くなっています。Markdown上では画像は![](url)と書くだけなので1行の高さしかないですが、プレビュー上では実際に画像を表示する高さになるので、スクロール速度を調整して、それぞれが表示しているものがずれないようにしているのです。

前提として

アルゴリズムを説明する前に、DocBase特有の事柄があるのでおことわりとしておきます。
DocBaseでは、サーバでMarkdownをHTMLに変換する処理をしていますが、この変換ライブラリは構文木(ASTや構想構文木)のようなスクロール同期に使えそうなデータを返しません。MarkdownをパースしてHTMLを返すのみです。そのため、本記事ではそれらの話は登場しません。

また、今回のアルゴリズムの実現には、エディタ部分のスクロール位置からその行の文字列を取得できる必要があります。エディタ部分がただのtextareaだった場合は、スクロール位置から文字の行数を求めるというようなことはできません。DocBaseではCodeMirrorというライブラリを使っているので、ライブラリのAPIによりスクロール位置からその位置に対応する行の文字列を取得することができています。

ボツ案:全体のスクロール比率で同期する

まずは、非常に簡単に実装できますが、実際に使ってみたらあまり使い勝手が良くなかった方法を紹介します。

アルゴリズムとしては、エディタのスクロールとプレビューのスクロールの比率を一致させるだけです。
エディタのスクロール比率は次の計算で導くことができます。

エディタのスクロール最大px = エディタ内のコンテンツ高さ - エディタ表示領域の高さ
エディタの現在のスクロール率 = エディタの現在のスクロールpx / エディタのスクロール最大px

そして、これを元にプレビュー画面のスクロール値を設定します。

プレビューのスクロール最大px = プレビュー内のコンテンツ高さ - プレビュー表示領域の高さ
プレビュー画面のスクロールpx = エディタの現在のスクロール率 * プレビューのスクロール最大px

これを実装したものを用意してみました。動かしてみてください。

動作サンプルはこちら

このサンプルだけみると問題無さそうです。
しかし、実際に業務などで使いうるMarkdown(たとえば、画像や大量のテキスト、大きいテーブルなどが入っている)と、それをHTMLとして表示したプレビューでこのスクロール同期をやってみると、MarkdownとHTMLの描画結果で大きく高さが異なる要素がある場合にスクロール位置がずれていくという現象が起こります。これでは使い勝手が悪く、場合によっては邪魔にしかなりません。

では、最初の添付画像のように良い感じに同期するためにはどのようにすれば良いのでしょうか。

採用案:特定要素間のスクロール比率で同期する

全体のスクロール比率で同期するというのは、使い勝手としては悪いということがわかりました。ですが、スクロール比率を利用するのは悪く無さそうです。
アルゴリズムを改善しましょう。

構文木を使わずに完全に全ての要素でスクロール同期を実現することはやはり難しいので、1つの制限を加えて問題を単純にします。全ての要素でスクロールが綺麗に同期することは諦め、<h1><h2>等々のヘッダのタグや、高さの変わりやすい<img>タグなどを基準に、スクロールが上手く同期するようにするのです。
この制限を加えた場合のスクロール同期のアルゴリズムはこうです。

  1. エディタ上の任意のスクロールy座標において、直近の要素のy座標と次の要素のy座標でのスクロール比率を求める
    • 直近の要素が無ければy座標は0とする
    • 次の要素が無ければy座標はスクロール領域の高さとする
  2. エディタ上の直近の要素を元に、プレビュー上からそれに対応する要素を探し出す
    • たとえば直近の要素がMarkdownで# タイトルと書かれている部分だったら、プレビューのHTMLから<h1>タイトル</h1>を見つける
  3. エディタ上の次の要素を元に、プレビュー上からそれに対応する要素を探し出す
  4. 1.で求めた比率を、2.3.で探し出した要素の間に掛けたものをプレビューのスクロール位置にする
    • プレビューのスクロール位置 = 2.で探し出した直近の要素のy座標 + 1.で求めた比率 * (2.で探し出した次の要素のy座標 - 2.で探し出した直近の要素のy座標)

これなら、Markdownの文とプレビューのHTML部分の高さがバラバラでも問題ないスクロールになります。

具体的な例を出して説明します。
次のようなMarkdownとプレビューのHTMLがあるとします。
途中にある緑の顏は、自分のgithubのアイコンです。自撮りです。

これに基準となるhタグやimgタグの位置に線を引くと、次のようになります。

左のエディタ部分と、右のプレビュー部分で同じ要素には同じ色の線を引きました。

  • 赤線
    • スタート位置
  • 緑線
    • h2タグの「あああ」の位置
  • 青線
    • imgタグの位置
  • 茶線
    • h2タグの「ううう」の位置

この時、エディタ側で青線と茶線の丁度半分ぐらいまでスクロールしたとします。
それに連動して、プレビュー側も青線と茶線の丁度半分までスクロールすれば良いのです。

これで完璧とはいきませんが、それっぽいスクロール同期にすることが出来ました。
応用して、同じような方法でプレビュー側でスクロールした場合もエディタ側のスクロール位置を適切にすることも出来そうですね。

宣伝

DocBase は社内・社外の垣根を超えて情報共有ができる情報共有サービスです。

細かい権限設定ができるので、オープンな情報もクローズドな情報も DocBase に集約できます。そして大きな企業さまでも全員で使えるよう、セキュリティには最も力を入れています。

チームを作成して30日間は無料ですべての機能が使えるので、ぜひチームを作成して試してみてください。

⇒ 30日間無料で DocBase を試してみる

  1. メモからはじめる情報共有 DocBase 無料トライアルを開始
  2. DocBase 資料をダウンロード

「いいね!」で応援よろしくお願いします!

このエントリーに対するコメント

コメントはまだありません。

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)


トラックバック

we use!!Ruby on RailsAmazon Web Services

このページの先頭へ