【WordPress】目次を自動表示するショートコードを自作する

WordPressの任意の投稿に目次を挿入する方法。プラグインは使用せず、ショートコードを定義して対応する。

目次を自動挿入するショートコード

functions.phpに下記のコードを追記する。

<?php


class Toc_Shortcode {

  private $add_script = false;
  private $atts = array();

  public function __construct() {
    add_shortcode('toc', array($this, 'shortcode_content'));
    add_action('wp_footer', array($this, 'add_script'), 999999);
    add_filter('the_content', array($this, 'change_content'), 9);
  }

  function change_content($content) {
    return "<div id=\"toc_content\">{$content}</div>";
  }

  public function shortcode_content($atts) {
    global $post;

    if (!isset($post))
      return '';

    $this->atts = shortcode_atts(array(
      'id' => 'toc',
      'class' => 'toc',
      'title' => '目次',
      'toggle' => true,
      'opentext' => '開く',
      'closetext' => '閉じる',
      'close' => false,
      'showcount' => 2,
      'depth' => 0,
      'toplevel' => 2,
      'scroll' => 'smooth',
    ), $atts);

    $this->atts['toggle'] = (false !== $this->atts['toggle'] && 'false' !== $this->atts['toggle']) ? true : false;
    $this->atts['close'] = (false !== $this->atts['close'] && 'false' !== $this->atts['close']) ? true : false;

    $content = $post->post_content;

    $headers = array();
    preg_match_all('/<([hH][1-6]).*?>(.*?)<\/[hH][1-6].*?>/u', $content, $headers);
    $header_count = count($headers[0]);
    $counter = 0;
    $counters = array(0, 0, 0, 0, 0, 0);
    $current_depth = 0;
    $prev_depth = 0;
    $top_level = intval($this->atts['toplevel']);
    if ($top_level < 1) $top_level = 1;
    if ($top_level > 6) $top_level = 6;
    $this->atts['toplevel'] = $top_level;

    // 表示する階層数
    $max_depth = (($this->atts['depth'] == 0) ? 6 : intval($this->atts['depth']));

    $toc_list = '';
    for ($i = 0; $i < $header_count; $i++) {
      $depth = 0;
      switch (strtolower($headers[1][$i])) {
        case 'h1':
          $depth = 1 - $top_level + 1;
          break;
        case 'h2':
          $depth = 2 - $top_level + 1;
          break;
        case 'h3':
          $depth = 3 - $top_level + 1;
          break;
        case 'h4':
          $depth = 4 - $top_level + 1;
          break;
        case 'h5':
          $depth = 5 - $top_level + 1;
          break;
        case 'h6':
          $depth = 6 - $top_level + 1;
          break;
      }
      if ($depth >= 1 && $depth <= $max_depth) {
        if ($current_depth == $depth) {
          $toc_list .= '</li>';
        }
        while ($current_depth > $depth) {
          $toc_list .= '</li></ul>';
          $current_depth--;
          $counters[$current_depth] = 0;
        }
        if ($current_depth != $prev_depth) {
          $toc_list .= '</li>';
        }
        if ($current_depth < $depth) {
          $class = $current_depth == 0 ? ' class="toc-list"' : '';
          $style = $current_depth == 0 && $this->atts['close'] ? ' style="display: none;"' : '';
          $toc_list .= "<ul{$class}{$style}>";
          $current_depth++;
        }
        $counters[$current_depth - 1]++;
        $number = $counters[0];
        for ($j = 1; $j < $current_depth; $j++) {
          $number .= '.' . $counters[$j];
        }
        $counter++;
        $toc_list .= '<li><a href="#toc' . ($i + 1) . '"><span class="contentstable-number">' . $number . '</span> ' . $headers[2][$i] . '</a>';
        $prev_depth = $depth;
      }
    }
    while ($current_depth >= 1) {
      $toc_list .= '</li></ul>';
      $current_depth--;
    }

    $html = '';
    if ($counter >= $this->atts['showcount']) {
      $this->add_script = true;

      $toggle = '';
      if ($this->atts['toggle']) {
        $toggle = ' <span class="toc-toggle">[<a class="internal" href="javascript:void(0);">' . ($this->atts['close'] ? $this->atts['opentext'] : $this->atts['closetext']) . '</a>]</span>';
      }

      $html .= '<div' . ($this->atts['id'] != '' ? ' id="' . $this->atts['id'] . '"' : '') . ' class="' . $this->atts['class'] . '">';
      $html .= '<p class="toc-title">' . $this->atts['title'] . $toggle . '</p>';
      $html .= $toc_list;
      $html .= '</div>' . "\n";
    }

    return $html;
  }

  public function add_script() {
    if (!$this->add_script) {
      return false;
    }

    $var = wp_json_encode(array(
      'open_text' => isset($this->atts['opentext']) ? $this->atts['opentext'] : '開く',
      'close_text' => isset($this->atts['closetext']) ? $this->atts['closetext'] : '閉じる',
      'scroll' => isset($this->atts['scroll']) ? $this->atts['scroll'] : 'smooth',
    ));

?>
    <script type="text/javascript">
      var xo_toc = <?php echo $var; ?>;
      let xoToc = () => {
        const entryContent = document.getElementById('toc_content');
        if (!entryContent) {
          return false;
        }

        /**
         * スムーズスクロール関数
         */
        let smoothScroll = (target, offset) => {
          const targetRect = target.getBoundingClientRect();
          const targetY = targetRect.top + window.pageYOffset - offset;
          window.scrollTo({
            left: 0,
            top: targetY,
            behavior: xo_toc['scroll']
          });
        };

        /**
         * アンカータグにイベントを登録
         */
        const wpadminbar = document.getElementById('wpadminbar');
        const smoothOffset = (wpadminbar ? wpadminbar.clientHeight : 0) + 2;
        const links = document.querySelectorAll('.toc-list a[href*="#"]');
        for (let i = 0; i < links.length; i++) {
          links[i].addEventListener('click', function(e) {
            const href = e.currentTarget.getAttribute('href');
            const splitHref = href.split('#');
            const targetID = splitHref[1];
            const target = document.getElementById(targetID);

            if (target) {
              e.preventDefault();
              smoothScroll(target, smoothOffset);
            } else {
              return true;
            }
            return false;
          });
        }

        /**
         * ヘッダータグに ID を付与
         */
        const headers = entryContent.querySelectorAll('h1, h2, h3, h4, h5, h6');
        for (let i = 0; i < headers.length; i++) {
          headers[i].setAttribute('id', 'toc' + (i + 1));
        }

        /**
         * 目次項目の開閉
         */
        const tocs = document.getElementsByClassName('toc');
        for (let i = 0; i < tocs.length; i++) {
          const toggle = tocs[i].getElementsByClassName('toc-toggle')[0].getElementsByTagName('a')[0];
          toggle.addEventListener('click', function(e) {
            const target = e.currentTarget;
            const tocList = tocs[i].getElementsByClassName('toc-list')[0];
            if (tocList.hidden) {
              target.innerText = xo_toc['close_text'];
            } else {
              target.innerText = xo_toc['open_text'];
            }
            tocList.hidden = !tocList.hidden;
          });
        }
      };
      xoToc();
    </script>
<?php
  }
}

new Toc_Shortcode();

ショートコードブロックで[toc]と入力すると、その投稿ページの目次が自動生成されるようになる。

カスタマイズCSSはこちら。

/* toc */
#toc {
  margin-top: 10px;
  margin-bottom: 35px;
  font-size: 15px;
  padding-right: 16px;
  padding-left: 16px;
  padding-top: 5px;
  border-top: 2px solid #2A403A;
  border-bottom: 2px solid #2A403A;
  display: inline-block;
}
#toc .toc-title {
  font-size: 16px;
  font-weight: bold;
  color: #2A403A;
  text-align: left;
  margin: 10px 0;
}
#toc .toc-title:before {
  content: '';
  display: inline-block;
  width: 18px;
  height: 18px;
  margin-right: 8px;
  background-image: url(icon/toc-16.svg);
  background-size: contain;
  vertical-align: middle;
}
#toc .toc-toggle a {
  color: #2A403A;
}
#toc ul {
  counter-reset: number;
  list-style: none;
  margin-bottom: 0px;
  padding-top: 5px;
  padding-left: 8px;
  padding-bottom: 5px;
}
#toc ul li {
  line-height: 1.25em;
  list-style-type: none;
}
#toc ul li a {
  color: #000;
  text-decoration: none;
  font-size: 14px;
}
#toc ul li a:hover {
  color: #aa4d41;
}
#toc ul ul li {
  line-height: 1.25em;
  margin-bottom: 0.5em;
  margin-left: 18px;
}
#toc ul ul li a {
  color: #000;
  padding-right: 6px;
  text-decoration: none;
  font-size: 14px;
}
#toc_container ul ul li a:hover {
  color: #aa4d41;
}
/* /toc */

備考

JavaScriptで目次を実装すると、表示のちらつきが発生したため、phpで作成した。

ショートコードのカスタマイズや、投稿全てに目次を自動挿入したい場合は、リンク先を参照すること。

参照

https://xakuro.com/blog/wordpress/277/

Related Tags