Zendeskで目次機能を実装した

# はじめに

これまで大きなZendeskカスタマイズの受託を2件ほど経験してきました。あまりZendeskのカスタマイズについて、個別具体的に解説している記事がないので、我流のカスタマイズで恐縮ですがここに残したいと思います。

### 「Zendeskとは？」　という方へ

私自身ZendeskのGuide機能しか取り扱っていないのですが、ざっくり説明すると「企業のヘルプセンターがノーコード（カスタマイズ等を実施する場合はローコード）　で構築できるよ！」というSaaSです。もっと詳しく知りたい方は[こちら](https://zeq.co.jp/case)を読んでみてください。

# Zendeskで目次機能を実装する

## 最終的に目指している状態

記事内の任意の場所に`<Toc/>`と記述することで、記事内のヘッダー要素を取得して下記のような目次を作成する。（前提としてjQueryを使用しています。）変数名の前に「$」を付けることで、その変数がjQueryオブジェクトを格納していることを明示しています。

### 記事の状態

#### 記事内の任意の場所に`<Toc/>`と記述してみた

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1953297/c1f12c27-ab94-994a-beea-bab25e2a0805.png)

#### 記事のソースコードのようす

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1953297/a5be1726-2810-39f2-30c3-cbb7a94c1925.png)

#### 画面描写の様子

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1953297/c9c9f9ea-fe2d-e625-d9b7-21dc3de6a3ad.png)

## 実際のソースコード解説

### 記事中の`<Toc/>`を取得して、HTMLタグに変換する

`<Toc/>`のままだと何も変換されないので、まずHTMLタグに変換します。
```
$('p:contains("<Toc/>")').replaceWith('<div id="toc"></div>');
```

### `.article-body`中のヘッダー要素を取得する

`h6`タグくらいまでのヘッダー要素を取得します。
```
var $headers = $('.article-body').find('h1, h2, h3, h4, h5, h6');
```

### 目次のベースとなるリスト要素を準備する

ヘッダー要素が複数確認できた場合にベースのHTML要素を設置します。
これらの処理は`$(document).ready(function () {})`内に設置します。
```
if ($headers.length > 1) {
    var $toc = $('<div class="toc">');
    var $firstUl = $('<ul>');
    var $currentUl = $firstUl;
    var previous_level = 1;
    $firstUl.appendTo($toc);
    $('#toc').append($toc);
    insertHeadings($headers);
}
```

### ヘッダー要素を繰り返しチェックしてネストさせていく

`insertHeadings`ではヘッダー要素をネストさせています。
```
function insertHeadings(headers) {
    headers.each(function(index, heading) {
        var $heading = $(heading);
        var current_level = headingLevel(heading);
        if (current_level === 1) {
            newLi($heading, $firstUl);
            $currentUl = $firstUl;
        } else if (current_level === previous_level) {
            newLi($heading, $currentUl);
        } else if (current_level > previous_level) {
            nestUl();
            newLi($heading, $currentUl);
        } else {
            while (current_level < previous_level) {
                $currentUl = $currentUl.parent().closest('ul');
                previous_level--;
            }
            newLi($heading, $currentUl);
        }
        previous_level = current_level;
    });
}
```

`nestUl`では追加の`ul`タグを設置しています。
```
function nestUl() {
    var $newUl = $('<ul>');
    $newUl.appendTo($currentUl);
    $currentUl = $newUl;
}
```

`headingLevel`ではヘッダーに対応する番号をリターンしています。
```
function headingLevel(heading) {
    switch (heading.nodeName) {
        case 'H1':
            return 1;
        case 'H2':
            return 2;
        case 'H3':
            return 3;
        case 'H4':
            return 4;
        case 'H5':
            return 5;
        case 'H6':
            return 6;
        case 'H7':
            return 7;
        case 'H8':
            return 8;
        case 'H9':
            return 9;
        case 'H10':
            return 10;
        default:
            return 0;
    }
}
```

`newLi`　では渡された要素を`li`要素として設置しています。
```
function newLi(heading, $list) {
    var $heading = $(heading);
    var $wrapper = $('<li></li>');
    var $link = $('<a>').prop('href', '#' + $heading.prop('id'));
    $link.html($heading.text());
    $link.appendTo($wrapper);
    $wrapper.appendTo($list);
}
```

# おわりに

当初は「どうやって実装するんだよ」状態だったんですが、ゴリゴリ実装してみると意外と実現できたので嬉しかったです。このカスタマイズに対して、もっとこうしたほうがいいとかアドバイスがあればよろしくお願いします！

ここまで読んでいただきありがとうございました。