Qiita スライドを Chrome 拡張で改善してみる

Qiita のスライド
=================

Qiita にはスライドビュー機能があります。
これを使えば Qiita の記事書きと、LTなどの発表で同じものが使えて非常に効率が良いですが、実際には機能が足りず、本番で使うには辛いものがあります。
そこで、足りない機能を全て Chrome 拡張で作ってみましたので、その作り方とともに公開します。

Chrome 拡張を通じてどのような修正を行うことで、改善が達成されるものなのか、参考にしてください。
また、この Chrome拡張も、そのソースも一般公開します。

なお、この Chrome 拡張では、jQuery v2 にくわえ、 `ChEx.js` という自作の秘伝のタレを使ってます。

--------------------------------------
Chrome拡張の置き場所
--------------------------------------

とりあえず入れて試したい方はこちらから入れてみてください。
https://chrome.google.com/webstore/detail/qiita-slide-%E6%94%B9/cgnhlhefahcojhfdoobggpdhpjcgfodc

（ソースコード全体は現在準備中です。近々公開します。）

--------------------------------------
1. **赤** や *青* で強調したい
--------------------------------------

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29899/b9ca4902-044d-c053-ed78-6a28a7b44fb0.png)

Qiita のスライドでは強調部分が単色で、強調が伝わりづらいですね。
そこで、`**～**` や `*～*` の部分に色がつくようにします。

```js
$('body').append(`<style>
    .slide_preview.markdownContent strong { color: red; }
    .slide_preview.markdownContent em { color: blue; font-style: normal; font-weight: bold; }
</style>`);
```

- `body` に `<style>～</style>` を足すことで全体的に効くスタイルを追加してます。
- この追加では既存の DOM のスタイルは変わってくれませんが、今後、新規に追加される DOM はすべてこの影響を受けることになります。
- `.slide_preview.markdownContent` は Slide の外枠で、この中にある `strong` や `em` の色を変えています。


----------------------------------------
2. カーソルを大きくしたい
----------------------------------------

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29899/071479ea-20cb-3b64-ab90-9fa9608d2e34.png)

カーソルつかって「ここです」と示しても、カーソル小さすぎて伝わりません。
そこで、 `B` キーで、カーソルを巨大化したり、もとに戻したりできるようにしましょう。

```js
$('body').append(`<style>
    .slide_preview.markdownContent.chex-big-cursor,
    .slide_preview.markdownContent.chex-big-cursor pre {
        cursor: url(https://waterada.github.io/chrome-ex-qiita/big-cursor.png), auto;
    }
</style>`);
ChEx.keydown('B', '大きなカーソル', () => {
    $('.slide_preview.markdownContent').toggleClass('chex-big-cursor');
});
```

- `ChEx.keydown()` という秘伝のタレを使っています。これで `B` を押した場合に `chex-big-cursor` がトグルするようにしてます。
- `ChEx.keydown()` の中では色々やってるのですが、要するに下記をやりたいのです。

```js
$(document).keydown(function (e) {
    if (e.which === 'B') {
        $('.slide_preview.markdownContent').toggleClass('chex-big-cursor');
    }
});
```

- `cursor` というスタイルを使ってカーソルを変えることができますが、普通大きさは変えられません。ですが画像は指定できますので、大きな画像を指定することで大きくしています。


----------------------------------------
3. 行番号オン/オフ
----------------------------------------

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29899/b1d9727d-68f4-c0b4-feb1-c28f40b5578c.png)

コードの説明の際に行番号が欲しくなるときがあります。
`L` キーで行番号のオン/オフできるようにしましょう。


```js
ChEx.keydown('L', '行番号オン/オフ', () => {
    let $preList = $('.slide_preview.markdownContent').find('pre');
    ChEx.toggle('lineNum', () => {
        $preList.each(function () {
            let $pre = $(this);
            let html = $pre.html();
            let i = 1;
            html = html.trim().replace(/^/gm, () => {
                return `<span class="chex-line-num">${ChEx.padding(i++, 3)}| </span>`;
            });
            $pre.html(html);
        });
    }, () => {
        $preList.find('.chex-line-num').remove();
    });
});
```

- `ChEx.toggle()` は２つのコールバックを呼び出すたびに交互に実行してくれる便利関数です。
- `ChEx.padding()` は数値のパディングをする便利関数です。
- 置換正規表現 `/^/gm` の `m` フラグは見慣れないかもしれません。`m` があると、`^` は各行の先頭にヒットするようになります。
- 現状、各行ごとに実際に改行されているので、先頭に連番の HTML を差し込んでいます。


----------------------------------------
4. 範囲選択エリアを黄色に
----------------------------------------

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29899/4d302233-174a-f728-35a9-3918160da9f6.png)

範囲選択して示してみても、デフォルトの色ではあまり目立ちません。
色を黄色マーカーっぽくしましょう。

```js
$('body').append(`<style>
    .slide_preview.markdownContent ::selection { background: yellow; color: black; }
    .slide_preview.markdownContent .chex-line-num::selection { background: #f7f7f7; color: black; }
</style>`);
```

- `::selection` というセレクタを指定することで色を変えられます。
- `.chex-line-num::selection` も指定しているのは、上記で行番号を表示してる場合、番号部分は色を変えたくなかったからです。


----------------------------------------
5. スライドコントローラのオン/オフ
----------------------------------------

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29899/c0800d20-c96e-9487-5230-9994e9ee8e8a.png)
↑スライドコントローラ

消して画面大きく見せたいとき、ありますね。

```js
ChEx.keydown('Q', 'スライドコントローラのオン/オフ', () => {
    $('.slide_controller').toggle();
});
```


----------------------------------------
6. keydown のヘルプ表示
----------------------------------------

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29899/54e023eb-a3b5-ffeb-c765-8cc156c27e3f.png)

`keydown` で仕込んだキー、忘れてしまうことがあるので、`H` キーでヘルプがでるようにしましょう。

```js
ChEx.keydownHelp('H', $('.slide_preview.markdownContent').position());
```

- `ChEx.keydown()` を使っていれば、勝手にヘルプ用のテキストを作り上げてくれます。
- `$('.slide_preview.markdownContent').position()` はヘルプ枠の表示位置を決める際の基準となる位置です。


----------------------------------------
7. h1で改行する
----------------------------------------

ページ内に h1 しかない場合、スライドだと、大きな文字で中央寄せで表示されるのですが、改行はできません。
そこで、半角スペース２つを書くことで改行されるようにします。

```js
//h1で半角スペース２つなら改行
$('body').append(`<style>
    /* チラツキ防止のため初め非表示 */
    .slide_preview.markdownContent h1 { display: none; }
    .slide_preview.markdownContent .slide_preview_firstSlide h1 { display: block; }
</style>`);
ChEx.onChangeDom($('.slide_preview.markdownContent'), function () {
    $('.slide_preview.markdownContent').find('h1').each(function () {
        let $h1 = $(this);
        $h1.html($h1.html().replace(/ {2}/g, '<br/>')).show();
    });
});
```

- `ChEx.onChangeDom()` は特定の DOM ノード以下で DOM の変更があった場合にイベントが発火するという便利関数です。
- 実際にやってることを簡潔に書くと次のようになります。

```js
ChEx.onChangeDom = function ($root, callback) {
    let timeoutId = 0;
    $root.on("DOMNodeInserted DOMSubtreeModified", function () {
        if (timeoutId) return true; //動いていたらやらない
        timeoutId = setTimeout(function () {
            try {
                callback();
            } finally {
                timeoutId = 0;
            }
            return true;
        }, 100);
    });
};
```

- `DOMNodeInserted`、 `DOMSubtreeModified` というイベントにハンドラを設定しているのがわかると思います。
- このイベントは連続的に発生することが多いので、0.1秒ほど落ち着くのを待ってから発火させてます。待ってる間に発生するイベントは全て捨てちゃいます。最後の状態で１回だけ発火すれば良いので。
- なぜ DOM の変更を監視してるのかというと、スライドの各ページは表示時に毎回、メモリ上に持っている情報をベースに生成されているためです。onLoad 完了後に DOM にあるものを書き換えても、ページに表示されるものは書き換わらないのです。
- `replace(/ {2}/g, '<br/>')` で空白２個を改行に変えてます。
- 最初に `display: none;` してるのは、表示後に書き換えることになるので、改行前の文字列が一瞬表示されて美しくないからです。


----------------------------------------
最後に
----------------------------------------

2017-08-05(土) に builderscon tokyo というイベントで 「Chrome拡張を使って様々なWebサービスをハックする」 というネタで登壇します。
https://builderscon.io/tokyo/2017/session/9e8711fb-7f56-44ea-b550-0d48ff756e1d

[![image.png](https://qiita-image-store.s3.amazonaws.com/0/29899/42de238d-a5c8-eb64-07bc-c7d3eb7f51ed.png)](https://builderscon.io/tokyo/2017/session/9e8711fb-7f56-44ea-b550-0d48ff756e1d)

ご興味ある方がいらっしゃいましたら、ぜひとも聞きにきてください。