トップへ戻る
BLOGS

WordPress テーマに目次機能を実装する

WordPress テーマに目次機能を実装する

こんにちは
以前他の記事を参考にしながら制作した「WordPressに自動で目次を出力する機能」をテーマの機能としてもう少し詳細設定などを加えて、いい感じにしたいなぁと思ってしまったので、そのための開発備忘録です

ちなみに目次を作成するには有名なプラグインが数多くあり、「Rich Table of Contents」や「Table of Contents Plus」などが有名で非常に有用でおしゃれですが、本記事はプラグインではなくテーマの中に「目次機能」を備えたい方に参考になる記事となっています

前回の記事

では毎度ながら、目次機能の作成概要をまとめたいと思います

目次機能の作成概要

  • テーマの中に目次機能を実装する
  • 管理画面上に目次の設定項目を追加する
  • デザイン設定をいくつか追加する
  • カラー設定をいくつか追加する
  • 目次のタイトル設定を追加する
  • 目次を開閉式にする
  • 目次に戻るボタンを実装し追従式にする
  • 目次に戻るボタンを非表示にできる
  • 表示条件の設定を追加する

なかなかハードルが上がりましたねぇ笑
さて恐れ多くも上から一つづつ実装していきます

テーマの中で目次機能を開発

まずはテーマの中で機能を実装するための環境構築です
とはいっても以前の記事にて作成した目次プラグインの機能をそのままテーマのfunctions.phpへ移設するだけです
まずはプラグインとして最終的に完成したコードのindex.phpから目次の関数のみを切り出してfunctions.phpへ転載します

functions.php

function oja_add_table_content( $content ) {
  if ( is_single() && in_the_loop() && is_main_query() ) {
    //h2 ~ h5を正規表現で表す
    $pattern = '/<h[2-5]>(.*?)<\/h[2-5]>/i';
    //$contentから要素を検索する
    preg_match_all( $pattern, $content, $matches, PREG_SET_ORDER );
    // h2~h5要素が3つ以上の場合に目次を出力
    if ( count( $matches ) >= 3 ) {
      // 目次出力に使用する変数
			$toc = '<p>この記事の目録</p>
              <ol class="postin-list">';
			// 目次の階層の判断に使用する変数
			$hierarchy = NULL;
			// ループ回数を数える変数
			$i = 0;
      foreach ( $matches as $element) {
        $i++;
        $id = 'toc_chapter'. $i;
        // preg_replace( 検索文字列, 置換文字列, 対象 ) ※正規表現でグループ化した部分を$1~3で置換できる ここではidをタグに挿入
        $chapter = preg_replace( '/<(.+?)>(.+?)<\/(.+?)>/',  '<$1 id="' . $id . '">$2</$3>', $element[0] );
        // idを挿入したタグでcontentを置換する
        $content = preg_replace( $pattern, $chapter, $content, 1);

        // それぞれのタグによる階層構造の設定
        // strpos(検索対象, 検索文字列) 返り値:検索文字列の位置を整数で返す
        switch ( true ) {
          case strpos( $element[0], '<h2' ) !== false:
            $tag_level = 2; break;
          case strpos( $element[0], '<h3' ) !== false:
            $tag_level = 3; break;
          case strpos( $element[0], '<h4' ) !== false:
            $tag_level = 4; break;
          case strpos( $element[0], '<h5' ) !== false:
            $tag_level = 5; break;
        }

        // ループ一回目
        if( $i == 1 ) {
          $hierarchy = $tag_level;

        // 次のタグが下層となる場合
        } elseif ( $hierarchy < $tag_level ){
          $current_tag = $hierarchy - $tag_level;
          if ($hierarchy == 2 && $tag_level == 4) {
            $toc .= PHP_EOL .'<ol class="e_level toc-h'. $tag_level .'"><ol>'. PHP_EOL;
          } elseif ($hierarchy == 2 && $tag_level == 5) {
            $toc .= PHP_EOL .'<ol class="e_level toc-h'. $tag_level .'"><ol><ol>'. PHP_EOL;
          } else {
            $toc .= PHP_EOL .'<ol class="toc-h'. $tag_level .'">'. PHP_EOL;
          }
          $hierarchy = $tag_level;

        // 次のタグが上層となる場合
        }	elseif( $hierarchy > $tag_level ){
          $current_tag = $hierarchy - $tag_level;
          // str_repeat(文字列, 繰り返す回数) ※返り値:繰り返された文字列
          $closing = str_repeat('</li>'. PHP_EOL .'</ol>', $current_tag );
          $toc .= $closing . PHP_EOL .'</li>'. PHP_EOL;
          $hierarchy = $tag_level;

        // 同じ階層がそれぞれ連続する場合
        } elseif( $hierarchy === $tag_level ){
          $toc .= '</li>'. PHP_EOL;
          $hierarchy = $tag_level;
        }

        // HTMLとして生成する
        $title = $element[1];
        $toc .= '<li><a href="#' . $id . '">' . $title . '</a>';
      } //foreach ( $matches as $element)
      // 閉じタグを出力
      $toc .= str_repeat('</li></ol>', $tag_level - 1);

      $index = '<nav class="postin-nav toc">
                  '. PHP_EOL . $toc .
                '</nav>'. PHP_EOL;

      $h2 = '/<h2.*?>/i';
      if (preg_match($h2, $content, $h2s)) {
        $content = preg_replace($h2, $index . $h2s[0], $content, 1);
      }
    } //if ( count( $matches ) > 3 )
  } //if ( is_single() && in_the_loop() && is_main_query() )
  return $content;
} ////function oja_add_table_content( $content )

余談ですが
私は「functions.php」がゴチャゴチャしていて視認性が悪いため、別ファイルにて読み込むようにしています
require_once locate_template('lib/mokuzi.php'); // 目次出力の関数

さて、これだけではまだ関数を定義しただけであるため実行されません
この関数をWordPressのフックにて登録して、いい感じのタイミングで実行してもらいましょう

lib/mokuzi.php (functions.php)

add_filter( 'the_content', 'oja_add_table_content', 10 );

ここまでコピペしてくるだけで、記事に自動で目次が出力されているはずです
CSSもコピペしてきて完全移設となりますが、このあとの設定項目の内容次第でいくらでも変更があるため、まずはドンドン機能面の実装をしていきます

管理画面上に目次の設定項目を追加する

今回はWordPressの管理画面上の「設定」の中にオリジナルの設定項目を追加していこうと思います

管理画面の「設定」にオリジナルの項目を追加するためにはWordPressが用意している関数やフックを使用します

管理メニューの追加 – WordPress Codex

add_options_page

公式の方法に則って実装していきましょう。
まずはフックでadd_options_page()を内包したメソッドを登録する必要があります

add_action( 'admin_menu', 'add_admin_mokuzi_menu' );

次のステップとしてadd_admin_mokuzi_menu関数を作成しadd_options_page()で設定項目を追加します

function add_admin_mokuzi_menu() {
  add_options_page(
    '目次設定', // 設定画面のページタイトル.
    '目次の設定', // 管理画面メニューに表示される名前.
    'manage_options',
    'oja_mokuzi_setup', // メニューのスラッグ.
    'oja_mokuzi_setup_page' // メニューの中身を表示させる関数の名前.
  );
}

これでフックに登録されたため、「設定」の項目の中に「目次の設定」が追加されたはずです

更にここからこの「目次の設定ページの中身」を出力するための関数oja_mokuzi_setup_pageを登録したためこの関数にフォームなどを記述していけば良さそうです

function oja_mokuzi_setup_page() {
  // 権限チェック.
  if ( ! current_user_can( 'manage_options' ) ) {
    wp_die( __( 'You do not have sufficient permissions to access this page.' ) );
  }
  ?>
  <div class="mokuzi_setting_wrap">
    <h1>オリジナルメニュー</h1>
  </div>
  <?php
}

これで管理画面上で確認して、設定が追加されておりそのページに入ると「オリジナルメニュー」というタイトルだけのページができているはずです

ここからはこの関数内にオリジナルの設定項目を追加していきます

設定画面を表示する

先の項目にて、add_option_page()関数にて定義したコールバック関数の中身を書き足して、管理画面を完成させます
書く内容が多いため一気にメソッドを書きます

/**
 * メニューページの中身を作成
 */
$selected_mokuzifont = [
  ['value' => 'gothick', 'content' => 'デフォルト(ゴシック体)'],
  ['value' => 'helvetica', 'content' => 'Helvetica'],
  ['value' => 'noto-sans-jp', 'content' => 'Google Noto Sans'],
  ['value' => 'minchou', 'content' => '明朝体'],
];
$selected_mokuzicolor = [
  ['value' => 'sunny-blue', 'color' => 'サニーブルー'],
  ['value' => 'citrus', 'color' => 'シトラス'],
  ['value' => 'fanny', 'color' => 'ファニー'],
  ['value' => 'modan', 'color' => 'モダン'],
  ['value' => 'cool', 'color' => 'クール'],
];
$selected_mokuzidesign = [
  ['value' => 'circle', 'design' => 'かわいい (デフォルト)'],
  ['value' => 'sinple', 'design' => 'シンプル'],
  ['value' => 'sharp', 'design' => 'シャープ'],
  ['value' => 'coolish', 'design' => 'クール(カラー設定変更不可)'],
];
function oja_mokuzi_setup_page() {
  // 権限チェック.
  if ( ! current_user_can( 'manage_options' ) ) {
    wp_die( __( 'You do not have sufficient permissions to access this page.' ) );
  }
  ?>
  <div class="wrap">
  <h2>おジャコの目録いじり</h2>
  <form method="post" action="options.php" enctype="multipart/form-data" encoding="multipart/form-data">
    <?php
    settings_fields('oja_mokuzi_group');
    do_settings_sections('oja_mokuzi_group'); ?>
    <div class="metabox-holder">
      <div class="postbox ">
        <h2 class='hndle'><span>基本設定</span></h2>
        <div class="inside">
          <h3>タイトル設定</h3>
          <p class="setting_description">目次のタイトルテキストを入力してください。</p>
          <p><input type="text" id="text" name="mokuzi_text" placeholder="Contents" value="<?php echo get_option('mokuzi_text'); ?>"></p>
          <h3>表示条件</h3>
          <p class="setting_description">見出しが個から目次を表示
            <input type="number" min="2" max="10" id="number" name="mokuzi_number" value="<?php echo get_option('mokuzi_number'); ?>"/>
          </p>
        </div>
      </div>
      <div class="postbox ">
        <h2 class='hndle'><span>デザイン設定</span></h2>
        <div class="inside">
          <h3>書式設定</h3>
          <p class="setting_description">目次の書式を選択してください。</p>
          <select name="mokuzi_select" id="select">
            <?php
            global $selected_mokuzifont;
            foreach ($selected_mokuzifont as $style) {
              echo '<option value="'. $style['value'] .'"'. selected($style['value'], get_option('mokuzi_select')).'>'.$style['content'].'</option>';
            } ?>
          </select>
          <h3>目次のテーマカラー</h3>
          <p class="setting_description">カラーテーマを選択してください。</p>
          <ul>
            <?php
            global $selected_mokuzicolor;
            foreach($selected_mokuzicolor as $color) {
              echo '<li><label><input name="mokuzi_color" type="radio" value="'.$color['value'].'"'.checked($color['value'], get_option('mokuzi_color'), false).'/>'.$color['color'].'</label></li>';
            } ?>
          </ul>
          <h3>目次のデザイン</h3>
          <p class="setting_description">デザインテーマを選択してください。</p>
          <ul>
            <?php
            global $selected_mokuzidesign;
            foreach($selected_mokuzidesign as $design) {
              echo '<li><label><input name="mokuzi_design" type="radio" value="'.$design['value'].'"'.checked($design['value'], get_option('mokuzi_design'), false).'/>'.$design['design'].'</label></li>';
            } ?>
          </ul>
        </div>
      </div>
      <div class="postbox ">
        <h3 class='hndle'><span>目次に戻るボタン</span></h3>
        <div class="inside">
          <p class="setting_description">目次に戻るボタンを表示する場合チェックを入れてください。</p>
          <label><input name="mokuzi_showcheck" id="checkbox1" type="checkbox" value="1" <?php checked(1, get_option('mokuzi_showcheck')); ?> />目次に戻るボタン</label>
        </div>
      </div>
    </div>
    <?php submit_button(); ?>
  </form>
</div>
  <?php
}

ちょっと長いですが解説していきます

管理画面作成の決り文句

管理画面のHTMLを出力するための決り文句なようなものが、あるため解説します

<form method=”post” action=”options.php”>
フォームタグのメソッドは「POSTであること」また送信先のaction属性は「options.php」にすること
settings_fields(‘定義するフィールドのスラッグ(任意)’);
Version 2.7 以前では、nonce magic, action field, そして page_options field が必須でしたが、この関数が面倒みてくれます。セキュリティ上の問題をこの一行にまとめたもので、必須となります。フィールドのスラッグは今回は任意ですが、後述するregister_settingの第一引数と揃える必要があります
do_settings_sections(‘上で定義したスラッグ’);
フォームのマークアップ全体を面倒見てくれる関数です。これも上の関数settings_fieldsとは必ずセットで使用するようにしましょう
<?php submit_button(); ?>
フォームの内容を確定するサブミットボタン。これを実行するだけで自動でいつも見る「設定を保存」ボタンが出力されます。その際この後設定するフォームの属性値に応じてデータベースに登録する処理のトリガーにもなります

タグのマークアップについて

全体のマークアップについてですが、今回指定しているタグとクラス名を当てると基本的にWPのそれなりのスタイリングはなされます。

そのため今回はそのスタイルをそのまま使用することにしましたが、目次機能をデザインするための設定項目であるため、Reactなどを使用しリアルタイムでDOMを更新して出来栄えを確認できる仕様が理想的であると思いますので、また次の機会にでも挑戦してみたいと考えています

各フォームの値について

各フォームは今回トップレベルで配列変数を作成しそれをforeach処理することで出力しました

<?php
global $selected_mokuzifont;
foreach ($selected_mokuzifont as $style) {
 echo '<option value="'. $style['value'] .'"'.selected($style['value'],     get_option('mokuzi_select')).'>'.$style['content'].'</option>';
} ?>

まずselectedcheckedというWordPressの関数を使用しています

これは第一引数に設定した値と、第二引数に設定した値を比べて同じであればHTMLのchecked属性やselected属性を出力してくれる便利な関数です

また各フォームのタグのname属性値が「wp_options」のデータベースに保存されるキーになりますので、これを取得する関数のget_optionのキーも同じになります

設定値をデータベースに登録する

ここからは先程設定したフォームにて入力があった項目の値を設定を保存ボタンを押した際にデータベースに登録処理するところを解説します

解説なんて言いましたがほとんどWordPressがやってくれます

/**
 * 設定値を登録保存処理
 */
function register_custom_setting() {
  register_setting('oja_mokuzi_group', 'mokuzi_text');
  register_setting('oja_mokuzi_group', 'mokuzi_number');
  register_setting('oja_mokuzi_group', 'mokuzi_select');
  register_setting('oja_mokuzi_group', 'mokuzi_color');
  register_setting('oja_mokuzi_group', 'mokuzi_design');
  register_setting('oja_mokuzi_group', 'mokuzi_showcheck');
// 初期値の設定
  add_option('mokuzi_text', 'Content');
  add_option('mokuzi_number', '2');
  add_option('mokuzi_select', 'gothick');
  add_option('mokuzi_color', 'sunny-blue');
  add_option('mokuzi_design', 'circle');
  add_option('mokuzi_showckeck', false);
}

register_setting関数で第一引数に先に指定していたsetting_fieldsのスラッグを指定します
第二引数の値は保存処理するための各フォームのname属性の値と同じにします

簡単に使用法です

register_setting(
 'field-group',// ① グループ名(settings_fieldsの値と同じにする)
 'item',	// ② 設定値の名前(inputのname要素に同じにする)
 'callback_func' // ③ サニタイズ関数
);
add_option( 'item', 'こんにちは');// ④ 初期値を設定

各設定の初期値を設定する

add_option()上の例文で少し紹介しましたが、この関数をinitフックで登録して各設定の初期値を登録することができます

add_option('登録するキー名', '値');

この後実装しますが、add_option や update_option で登録したデータベースの値には以下の方法で取得することができるため、この後使用していきます

get_option('上記で設定したキー名');

ここまでで管理画面に以下のように設定項目が完成しそれを保存するための処理も実装できました

管理画面の画像

さて次の項目ではこれらのデータベースに保存した値を使用した目次のカスタマイズを行っていきます

目次生成コードの改変

それではこれまで管理画面で登録してきた値を使用してテーマの中で目次機能を開発の章で作成していたPHPの目次出力方法に手を加えていきましょう

mokuzi.php

<?php
// 管理画面settingの読み込み
require_once locate_template('/lib/admin_setting/mokuzi_setting.php');

// 見出しのidを付け替える
function add_chapter_id($the_content) {
  if ( is_single() && in_the_loop() && is_main_query() ):
    $pattern = '/^<h([2-5]).*?.+?<\/h[2-5]>$/im';
    if (!preg_match_all($pattern, $the_content, $headings, PREG_SET_ORDER)) {
      //見出しがないときは終了
      return $the_content;
    }
    /*
    $headings:見出しの配列
    $headings[0]:array(2)=> "最初の見出し", h2なら"2"のセット
    */
    // $setcount = oja_option();
    // h2~h5要素が3つ以上の場合に出力(デフォルト)
    $headings_count = get_option('mokuzi_number') ? get_option('mokuzi_number') : 3;
    if(count($headings) >= $headings_count) :
      // ループ回数を数える変数
      $i = 0;
      foreach ( $headings as $element) {
        $i++;
        $id = 'toc_chapter'. $i;
        // preg_replace( 検索文字列, 置換文字列, 対象 ) ※正規表現でグループ化()した部分を$1~3で置換できる ここではid含むタグを作成し
        $chapter = preg_replace( '/<(.+?)>(.+?)<\/(.+?)>/',  '<$1 id="' . $id . '">$2</$3>', $element[0] );
        // idを挿入したタグで本文を置換する
        $the_content = str_replace( $element[0], $chapter, $the_content);
      }
    endif;
  endif;
  return $the_content;
}
add_filter('the_content', 'add_chapter_id', 1);

function oja_add_mokuzi( $the_content ) {
  if ( !is_single() || !in_the_loop() || !is_main_query() ) {
    return $the_content;
  }
  //h2 ~ h5をidごと正規表現で表す
  $pattern = '/^<h([2-5]).+?id\s*=\s*"(.+?)".*>(.+?)<\/h[2-5]>$/im';
  if (!preg_match_all($pattern, $the_content, $toc_headings)) {
    return $the_content;
  }
  /*
  $toc_headings[0]:マッチした文字列<h2>text</h2>
  $toc_headings[1]:見出しレベルの数字 "2"
  $toc_headings[2]:id設定値
  $toc_headings[3]:見出し文
  */
  // h2~h5要素が3つ以上の場合に目次を出力
  $headings_count = get_option('mokuzi_number') ? get_option('mokuzi_number') : 3;
  $heading_title = get_option('mokuzi_text') ? get_option('mokuzi_text'): '目次';
  if(count($toc_headings) >= $headings_count) :
    // 目次の設定に応じてクラス名を付与
    $toc_class = '';
    if(get_option('mokuzi_color')) {
      $toc_class .= ' '.get_option('mokuzi_color');
    }
    if(get_option('mokuzi_select')) {
      $toc_class .= ' '.get_option('mokuzi_select');
    }
    if(get_option('mokuzi_design')) {
      $toc_class .= ' '.get_option('mokuzi_design');
    }
    $level = '';
    $hierarchy = 0;
    $toc = '<nav class="mokuzi_wraper '.$toc_class.'"><dl class="oja_toc" id="oja_toc">';
    $toc .= '<dt class="toc_title">'.$heading_title;
    $toc .=   '<span id="mokuzi_show_toggle">Close</span>';
    $toc .= '</dt><dd class="mokuzi_content"><ol>';
    $count = count($toc_headings[0]);
    for ($i = 0; $i < $count; $i++) {
      // strpos(検索対象, 検索文字列) 返り値:検索文字列の位置を整数で返す
      switch ( true ) {
        case strpos( $toc_headings[1][$i], '2' ) !== false:
          $level = 0;  break;
        case strpos( $toc_headings[1][$i], '3' ) !== false:
          $level = 1; break;
        case strpos( $toc_headings[1][$i], '4' ) !== false:
          $level = 2; break;
        case strpos( $toc_headings[1][$i], '5' ) !== false:
          $level = 3; break;
      }
      //階層がネストされるとき
      while ($hierarchy < $level) {
        $toc .= '<ol class="toc_child">';
        $hierarchy++;
      }
      //閉じるとき
      while ($hierarchy > $level) {
        $toc .= '</li></ol></li>';
        $hierarchy--;
      }
      //aタグでタイトルを囲む
      $toc .= '<li><a href="#' . $toc_headings[2][$i] . '">' . $toc_headings[3][$i] . '</a>';
    } //for
    $toc = $toc . '</ol></dd>';
    // 目次に戻るボタンの分岐
    if(get_option('mokuzi_showcheck')) {
      $toc .= '<span id="back_to_mokuzi"><a href="#oja_toc">目次へ</a></span>';
    }
    $toc .= '</dl></nav>';
    //最初のh2の前に$tocをつけた本文に置換する
    $the_content = str_replace($toc_headings[0][0], $toc . $toc_headings[0][0], $the_content);
  endif; //if ( count( $toc_headings ) > 3 )
  return $the_content;
} //function oja_add_mokuzi( $content )
add_filter( 'the_content', 'oja_add_mokuzi', 8 );

今回は機能のアップデートということでコードの見直しも兼ねて、以前の記事と少し出力方法を変更しております

トップレベルではlocate_template()によって前項までで作成したsetting-formを読み込んでいます
コードの視認性を上げるため、「見出しタグにid属性を付与する関数」と「目次を作成しコンテンツに表示する関数」と切り分けることにしました。

目次の表示設定で用意しておいたデータを取得して、ループカウンターの条件に使用します

 $headings_count = get_option('mokuzi_number') ? get_option('mokuzi_number') : 3;

id付与関数と目次表示関数を分けたことで表示関数の方の見出しタグの取得方法を変更しております

$pattern = '/^<h([2-5]).+?id\s*=\s*"(.+?)".*>(.+?)<\/h[2-5]>$/im';


そして()で囲われた部分は複数の項目を1つの部分表現(サブパターン)にまとめる(グループ化する)という意味があります
簡単に説明するところ、「見出しタグの数字」、「id属性値」、「インナーテキスト」の3グループに分けて検索を行うことができます

preg_match_all()の返り値

複雑なので以下にまとめました。実装する際にはぜひ自身でvar_dump()しながらご確認ください

  • $toc_headings[0]:マッチした文字列 = <h2>text</h2>
  • $toc_headings[1]:見出しレベルの数字 = “2”
  • $toc_headings[2]:id設定値 = “toc_chapter1”
  • $toc_headings[3]:見出し文 = “text”

データベースから値を取得してHTMLへ変換する

string型のデータはそのままクラス名やテキストとして使用します。以下の部分です

// 目次の設定に応じてクラス名を付与
 $toc_class = '';
 if(get_option('mokuzi_color')) {
   $toc_class .= ' '.get_option('mokuzi_color');
 }
 if(get_option('mokuzi_select')) {
   $toc_class .= ' '.get_option('mokuzi_select');
 }
 if(get_option('mokuzi_design')) {
   $toc_class .= ' '.get_option('mokuzi_design');
 }

WordPressが自動でサニタイズしてくれているとのことで今回はそのまま使用しました

真偽値のデータもそのまま条件分岐に使用します。以下の部分です

 // 目次に戻るボタンの分岐
 if(get_option('mokuzi_showcheck')) {
   $toc .= '<span id="back_to_mokuzi"><a href="#oja_toc">目次へ</a></span>';
 }

ここまででなかなかコード量が多くなってきておりますが、なんとか管理画面の設定から、設定値を利用した目次の生成コードまでが完成しました。
ここからは各設定値によってスタイリングを出し分ける作業になります

JSでイベントを登録する

SCSSのスタイリングに入る前にJavaScriptで動的な部分を実装していきます。

目次の開閉ギミックの実装

まずは目次の開閉ギミックの実装方法を解説します
やりたい具体的な内容は以下のような内容です

  • 目次開閉ボタンをクリックしたらクラス名をつけ外しする
  • 目次開閉ボタンをクリックしたらボタンの文字を変更する

ということで以下のコードで実装しました

main.js

// 目次の開閉スイッチ
const mokuziBotton = document.getElementById('mokuzi_show_toggle');
const mokuziContent = document.querySelector('.toc_title').nextElementSibling;
if(mokuziBotton) {
  mokuziBotton.addEventListener('click', () => {
    mokuziContent.classList.toggle('closed');
    mokuziBotton.textContent = "Close";
    if(mokuziContent.classList.contains("closed")) {
      mokuziBotton.textContent = "Open";
    }
  })
}

要素を取得してきたらclassList.toggleでクラス名の付替えを行い、textContentでHTMLの中身の文字列をリセットし、classList.containsにてクラス名の条件判定を行い中の文字列を入れ替えています

目次に戻るボタンをスクロールで表示する

次は「目次に戻るボタン」が表示されている場合に、現在見ているページの位置が目次のコンテンツより低い場合にのみこのボタンを表示させるための実装です

ボタンはCSSのposition: fixed; でページ内に固定で配置するため、ページトップ位置で表示されてしまっていたら、UXとして不親切だからです

以下のコードで実装しました

main.js

// 目次へ戻るボタンの設定
const mokuziBackButton = document.getElementById('back_to_mokuzi');
if (mokuziContent && mokuziBackButton) {
  document.addEventListener('scroll', ()=> {
    contentDistans =
      mokuziContent.getBoundingClientRect().bottom -
      mokuziContent.clientHeight * 0.2;
    if(contentDistans < 0) {
      mokuziBackButton.classList.add('show');
    } else {
      mokuziBackButton.classList.remove('show');
    }
  })
}

これの実装概要としてはまず、DOMそのものにイベントリスナーを登録します

そして目次コンテンツの要素の大きさを変数にしておき、その目次要素が画面に対して最上部で見切れる位置を下回ったら、classListメソッドでクラス名を付与。それ以外の場合ではクラス名を削除するように実装します

目次のスタイリング

さて全ての準備が整ったここからはそれぞれのクラス名を使用したスタイリングを行っていきます

CSS設計の考え方としては、、

  • 前提:ラッパー要素に追加されるクラス名
  • 基本となるスタイリングを記述
  • デザインクラス名に応じて各要素 (liや::before)などのスタイリングを変更
  • @mixinで全てのクラス名を網羅したカラー設定を定義
  • カラークラス名に応じて@mixinをinclude

この流れで見ていきます
以下は完成事例です

_mokuzi.scss

@mixin  tocThemeColor($color) {
  &.sinple {
    border: 2px solid $color;
  }
  &.coolish {
    border-top: 10px solid $color;
    border-bottom: 10px solid $color;
    background: #F3F3F2;
  }
  &.sharp {
    li::before {
      color: $color;
    }
  }
  dl.oja_toc {
    background-color: rgba($color,0.1);
    dt.toc_title {
      border-color: rgba($color, 0.9);
      &::before {
        background-color: $color;
      }
    }
    li::before {
      background-color: $color;
    }
    li::after {
      color: $color;
    }
    a:hover {
      color: $color;
    }
  }
  span#back_to_mokuzi {
    background-color: $color;
  }
}

nav.mokuzi_wraper {
  width: 85%;
  margin: 3rem 5%;
  position: relative;
  font-family: "ヒラギノ角ゴ Pro W3",
    "Hiragino Kaku Gothic Pro",
    "メイリオ",
    "Meiryo",
    sans-serif;
  @include md {
    width: 90%;
    margin-bottom: 2rem;
  }
  @include sm {
    margin: 0 auto;
    width: 100%;
    margin-bottom: 2rem;
  }

  /**
  * 書式設定
  */
  &.gothick {
    font-family:"ヒラギノ角ゴ Pro W3",
    "Hiragino Kaku Gothic Pro",
    "メイリオ",
    "Meiryo",
    sans-serif;
  }
  &.helvetica {
    font-family: Helvetica, Arial, sans-serif;
  }
  &.noto-sans-jp {
    font-family: 'Noto Sans JP', sans-serif;
  }
  &.minchou {
    font-family:  游明朝体, "Yu Mincho", YuMincho, "ヒラギノ明朝 Pro", "Hiragino Mincho Pro", "MS P明朝", "MS PMincho", serif;
  }

  /**
  * Design設定
  */
  &.sinple { // シンプル
    background: #f7f7f3;
    dt.toc_title {
      border: none;
    }
    li::before {
      background-color: transparent !important;
      color: #444444;
      font-size: 16px;
      content: counter(li)":";
    }
    .toc_child {
      li {
        padding-left: 2%;
      }
      li::before {
        content: none;
      }
    }
  }
  &.sharp { // シャープ
    dl.oja_toc {
      @include md {
        padding-left: 1.5rem;
      }
      @include sm {
        padding-left: 1rem;
      }
    }
    dt.toc_title {
      border: none;
    }
    ol {
      counter-reset: section;
      padding-top: 1%;
      li {
        padding-left: 6%;
        padding-bottom: 1%;
        @include md {
          padding-left: 5%;
        }
        @include sm {
          padding-bottom: 3%;
          padding-left: 7%;
        }
        &:before {
          counter-increment: section;
          content: counters(section, ".");
          background: transparent !important;
          color: #555;
          font-size: 1.2rem;
          @include md {
            font-size: 14px;
          }
        }
        &:after {
          content: '|';
          width: 30px;
          height: 30px;
          position: absolute;
          left: 20px;
          top: -10px;
          font-size: 30px;
          @include lg(1100px) {
            left: 15px;
          }
          @include md {
            font-size: 25px;
            top: -8px;
            left: 10px;
          }
          @include sm {
            font-size: 20px;
            top: 4px;
          }
        }
      }
      .toc_child {
        li {
          padding-left: 8%;
          @include sm {
            padding-left: 10%;
          }
        }
        li::before {
          top: 6px;
          font-size: 15px;
          @include md {
            font-size: 12px;
          }
          @include sm {
            font-size: 10px;
          }
        }
        li::after {
          left: 30px;
          font-size: 25px;
          @include lg(1100px) {
            left: 25px;
          }
          @include md {
            font-size: 20px;
            top: -4px;
          }
          @include sm {
            font-size: 15px;
            top: 5px;
            left: 20px;
          }
        }
        .toc_child {
          li {
            padding-left: 5px;
          }
          li::before {
            content: '';
          }
          li::after {
            font-size: 10px;
            transform: rotate(90deg);
            left: -25px;
            top: 15px;
          }
          a {
            font-size: 0.8rem;
          }
        }
      }
    }
  }

  &.coolish {
    border-top: 10px solid #444444;
    background: #F3F3F2;
    dt.toc_title {
      border: none;
      &::before {
        background-color: #444444;
      }
    }
    li::before {
      background-color: transparent !important;
      color: #444444;
      font-size: 16px;
      content: counter(li)".";
    }
    .toc_child {
      li::before {
        content: '-';
        top: 6px;
      }
    }
  }
  // 目次テーマ設定
  &.sunny-blue {
    @include tocThemeColor(#3f9cff);
  }
  &.citrus {
    @include tocThemeColor(#FDC15D);
  }
  &.fanny {
    @include tocThemeColor(#f6b8c8);
  }
  &.modan {
    @include tocThemeColor(#405796);
  }
  &.cool {
    @include tocThemeColor(#444444);
  }
}

dl.oja_toc {
  padding: 2rem 0.5rem 2.5rem 3.5rem;
  @include md {
    padding: 2rem 0rem 2.5rem 2.5rem;
  }
  @include sm {
    padding: 2rem 0rem 2.5rem 1.5rem;
  }
  dt.toc_title {
    font-weight: 700;
    letter-spacing: 1.8px;
    margin-bottom: 20px;
    color: #555;
    font-weight: bold;
    font-size: 1.6rem;
    transform: translateX(-20px);
    border-bottom: 5px dotted;
    padding-bottom: 10px;
    border-color: #3f9cff;
    position:relative;
    @include md {
      font-size: 1.2rem;
      padding-left: 20px;
    }
    @include sm {
      padding-bottom: 5px;
      padding-left: 0.6rem;
      transform: translateX(-14px);
    }

    &::before {
      display: inline-block;
      font-weight: 900;
      font-family: "Font Awesome 6 Free";
      content: "\f03a";
      margin-right: 10px;
      width: 50px;
      height: 50px;
      text-align: center;
      background: #3f9cff;
      border-radius: 50%;
      color: #fff;
      @include md {
        width: 35px;
        height: 36px;
      }
      @include sm {
        width: 30px;
        height: 30px;
        font-size: 15px;
        transform: translateY(-3px);
      }
    }

    // 目次開閉ボタン
    #mokuzi_show_toggle {
      font-weight: 400;
      cursor: pointer;
      font-family: Menlo,Monaco,Consolas,"CourierNew",monospace;
      font-size: 1rem;
      color: gray;
      position:absolute;
      right: 20px;
      top: 10px;
      display: block;
      @include md {
        font-size: .8rem;
      }
    }
  }
  dd.mokuzi_content {
    transition: $anime;
    height: auto;
    opacity: 1;
    visibility: visible;
    &.closed {
      height: 0;
      visibility: hidden;
      opacity: 0;
    }
  }

  // 目次に戻るボタン
  span#back_to_mokuzi {
    position: fixed;
    z-index: 3;
    bottom: 70px;
    left: 16px;
    width: 58px;
    height: 58px;
    padding-top: 33px;
    box-sizing: border-box;
    border-radius: 50%;
    text-align: center;
    transition: $anime;
    opacity: 0;
    visibility: hidden;
    @include md {
      width: 45px;
      height: 45px;
      padding-top: 20px;
    }
    &.show {
      opacity: 1;
      visibility: visible;
    }
    a {
      color: #fff;
      font-size: 0.6em;
      text-decoration: none;
      &::before {
        position: absolute;
        content: "\f0ae";
        font-weight: 900;
        font-family:"Font Awesome 6 Free";
        top: 2px;
        left: 16px;
        width: 24px;
        height: 22px;
        font-size: 20px;
        @include md {
          font-size: 14px;
          left: 11px;
        }
      }
    }
  }

  ol {
    margin: 0;
    padding: 0;
    list-style: none;
    counter-reset: li;
    li {
      position: relative;
      padding-left: 4%;
      @include md {
        padding-left: 5%;
        padding-bottom: 1%;
      }
      @include sm {
        padding-left: 8%;
        padding-bottom: 3%;
        line-height: 20px;
      }
      &:not(:first-child) {
        margin-top: 0.5em;
      }
      &::before {
        counter-increment: li;
        content: counter(li);
        position: absolute;
        left: -5px;
        top: 10px;
        display: block;
        width: 20px;
        height: 20px;
        border-radius: 50%;
        background-color: #3f9cff ;
        font-size: 12px;
        color: #fff;
        line-height: 20px;
        text-align: center;
        @include md {
          font-size: 10px;
          width: 18px;
          height: 18px;
          top: 7px;
          line-height: 18px;
        }
        @include sm {
          top: 5px;
        }
      }
      a {
        font-size: 1rem;
        font-weight: 400;
        text-decoration: none;
        display: inline-block;
        position: relative;
        color: #707070;
        text-decoration: none;
        transition: all 0.3s;
        @include md {
          font-size: .8rem;
        }
        &:hover {
          text-decoration: underline;
        }
      }
    }
  }
  .toc_child {
    li::before {
      width: 10px;
      height: 10px;
      left: 3px;
      top: 13px;
      content: '';
    }
    a {
      font-size: 0.8rem;
    }
  }
}

今回はこんな感じの実装になりました
もちろん世の中素晴らしいデザインの目次は無数にあるので、これをさらに改良、組み替えて各々素晴らしいコンテンツを実装していただければ幸いです

今回の学び

前回作成した目次生成プラグインから随分時間も立っており、うる覚えな状況の中で何回も自身の記事やドキュメントとにらめっこしながらの実装になりました 苦笑

テーマの中に新たな機能を組み込む

当たり前ですが、テーマの中に新たな機能を実装するということはそのための設計が必要になりました、、

  • データベースを使用するのか
  • 管理画面が必要な機能なら、管理画面上にどう実装するか

これらは理解こそしていましたが、実際に実装した経験は乏しかったので新鮮でした
実務であれば顧客の要望や管理面などから、フロントエンド並に気を使う場所であると思いました
それほど重要度が高い要点でした

管理画面のカスタマイズ

管理画面のカスタマイズ方法は完全に忘却の彼方え消えてなくなっておりました 笑

確か以前はwp_nonceなどを使用し古より伝わる古典的な方法で実装したという経験はありましたが、専用関数でここまで簡単になっているとは、WordPress流石です

phpの正規表現、、、、

正規表現難しいです、、そもそもあまり使用頻度が高くない、、php自体久しぶり、、

しかしこんな状況でも「正規表現チェッカー」など先任者の素晴らしい公共財産を利用させていただくことで次第に、扱いやすくなっていっている状況を感じました
正規表現を1から100まで網羅的にしっかり学ぶのではなく、必要になったときにすぐに目的の表現を取得できるスキルが大事であると感じました

今回もお疲れさまでした(^^)

コメントをお待ちしております

お気軽にコメントをどうぞ。

CAPTCHA