スクロール位置同期ができないなら見出し同期にすればいいじゃない

## はじめに
マークダウンエディタとプレビュー間のスクロール位置同期を実装できなかったので、代わりに見出し同期を採用した、というアイデアノートです。

例によって「かんたんMarkdown」を実装したときのアイデアです。

> かんたんMarkdown
> https://tatesuke.github.io/KanTanMarkdown/

## スクロール位置自動同期とは
「かんたんMarkdown」というMakrodwnエディタをHTML + JavaScript + CSSで作っています。他のMarkdownエディタでもよく見る画面左右分割型で、左がMarkdown編集画面、右がプレビューという構成になっています。Qiitaや[StackEdit](https://stackedit.io/)と同じタイプですね。

![左右分割.png](https://qiita-image-store.s3.amazonaws.com/0/29682/61e5b58a-735e-662d-c748-5f4e7755af7a.png)


左のエディタで編集しつつ、右のプレビューで内容や見た目を確認するということになるので、当然ながら左と右で同じ箇所を表示したいわけです。左の編集内容と右のプレビューが別々の場所を表示していたら分かりづらいし、表示箇所がずれるたびにいちいち手動で合わせるのは激しく面倒です。

そのため、QiitaやStackEditではエディタとプレビューのスクロール位置を自動的に同期するという手法をとっています（これを私はスクロール位置自動同期と呼んでいます）。なにそれ便利。

![スクロール同期.gif](https://qiita-image-store.s3.amazonaws.com/0/29682/8218c601-f47b-35ec-da49-3dce4c46f66f.gif)


## 実装技術がない
大変便利なので「かんたんMarkdown」にもスクロール位置自動同期を実装しようと思い、すぐにエディタを開きました。が、肝心の実装方法が思いつきませんでした。

単純にスクロールバー位置を合わせればいいか程度に考えていたのですが。エディタの高さとプレビューの高さは違いますからそう簡単にはいきません。

唯一考えられたのは「自分でマークダウンをパースする」ということでした。自分でマークダウンをパースすれば、Markdownテキストと変換後のHTMLのどことどこが対応するのかエレメントレベルで分かるからです。でも、そんな面倒なことはしたくないというのと、そもそも**パーサを実装する技術が自分にはなさそう**なので不採用でした。


## 見出し同期というアイデア
全く同じような機能は諦めるとして、何か別のアイデアはないか。そう思って考え出したのが「**見出し同期**」です。「見出し同期」は私が勝手につけた名前で、要するに次のような機能です


* エディタで文章を書いているときに見出し同期ボタン（またはF2キー）を押すと、プレビューはその文の見出し位置にスクロールする
* プレビューで文章を読んでいるときに見出しをクリックすると、エディタがそこにスクロールする

![見出し同期.gif](https://qiita-image-store.s3.amazonaws.com/0/29682/a3540579-2082-2ab2-da0a-9e4cecf6e15d.gif)



## 仕組み
どのように見出し同期を実現したか簡単に説明します。すごく単純で、エディタやプレビュー上の見出しの数を数えているだけです。

だいたいこんな感じです。

### 編集中にF2キーを押した場合
1. 現在の行が、何個目の見出しの配下にあるか数える  
   (仮にn個目の見出し配下だったとする)。
1. プレビュー内のn個めの見出しのエレメントを取得し、スクロールバーの位置を合わせる。

```js:コードイメージ
var n = 何番目の見出し配下か数える();

var hElems = previewer.querySelectorAll("h1, h2, h3, h4, h5, h6");
var elem = hElems[n - 1];
previewer.scrollTop = elem.offsetTop - previewer.offsetTop;
```

何個目の見出し配下にいるかを数えて
![見出し同期1.png](https://qiita-image-store.s3.amazonaws.com/0/29682/df029d71-36d3-1ebb-0c3b-d953cf6e0cfc.png)
　↓↓
同じ個数目のh1～h6エレメントまでスクロールする
![見出し同期2.png](https://qiita-image-store.s3.amazonaws.com/0/29682/fd5e394e-6cc1-1145-25dc-974ab440f776.png)


### プレビューの見出しをクリックした場合
上記の逆です。

1. クリックされた見出しが、何個目の見出しであるか数える
1. エディタ上のn番目の見出し行を探し、キャレットを移動させる。

```js:コードイメージ
document.querySelectorAll("h1, h2, h3, h4, h5, h6")
		.addEventListener("click", function(){
	
	// 何番目の見出しか数える
	var headings = previewer.querySelectorAll("h1, h2, h3, h4, h5, h6");
	var n;
	for (n = 0; n < headings.length; n++) {
		if (headings[n] == elem) {
			break;
		}
	}
	n++;
	
	エディタの見出しを選択(n)
});
```

何番目のh1～h6エレメントがクリックされたか数えて
![見出し同期3.png](https://qiita-image-store.s3.amazonaws.com/0/29682/d91ed289-b71b-3db9-050a-792b6c546de3.png)

　↓↓

同じ個数目のヘッダの行にキャレットを移動する
![見出し同期4.png](https://qiita-image-store.s3.amazonaws.com/0/29682/ed051f74-9f08-03fa-8c01-847b7b6bc67b.png)


Markdownテキストの見出し行を判定するには若干の工夫が必要でしたが、それでもMarkdownパーサーを作るよりは何十倍も単純なコードで実装できたと思います。

実際のソースは[このあたり](https://github.com/tatesuke/KanTanMarkdown/blob/39a4d9cc0ef898da765581bd97fba794b7eaee8b/src/js/kantanMarkdown.js#L1243)です。

<small>
そういえばキャレットを移動しただけではテキストエディタのスクロールバーが追随しないなどの問題はありましたが、それはまた別のお話・・・

> 【黒魔術】テキストエリアのキャレット位置をjsで指定した時にスクロールバーを追従させる
> http://qiita.com/tatesuke/items/b3df2e263e5ca4dee138
</small>

## おわりに
スクロール位置自動同期は私の能力では実装できませんでしたが、同期位置の対象を見出しだけに絞れば私にも実装できました。単純で素朴な機能ですがその効果は絶大で、すごく便利です。

そのままは無理でもちょっと視点を変えれば便利なものが作れるんだなということで、なにかのアイデアのヒントにでもなれば幸いです。