プラグインなしで「お気に入り」機能を追加する方法(ローカルストレージ版)

Cocoonカスタマイズ集
この記事は約36分で読めます。
記事内に広告が含まれています。

Ajax(非同期通信)で表示を更新する方法

動作シーケンス

JS⇔PHP間の動作シーケンスを以下にまとめました。

初回ページ読み込み

ブラウザ (JS)
サーバー (PHP)
1
1. ページをリクエスト
2
2. ウィジェットを空(0件)に初期化

通常の新着記事が一瞬表示されるのを防ぐため、クエリを空にする

3
3. 空のウィジェット枠を含むHTMLを返却
ここで画面が表示される(中身はまだ空)
4
4. JS起動 & ローカルストレージ参照

お気に入りID(例: [123, 456])を読み出す

5
5. Ajax POSTでIDをリクエスト
6
6. ショートコード実行 & リストHTML生成

届いたIDを元に、お気に入り記事だけのHTMLを組み立てる

7
7. 生成したリストのHTMLを返却
8
8. 枠内にリストを流し込み & ボタン同期

空だったお気に入り枠に記事を書き込み、ボタンが点灯して完成

お気に入りボタンクリック(追加・削除)

ブラウザ (JS)
サーバー (PHP)
読者がボタンをクリック

お気に入り追加/削除のトリガーが発生。

1
1. クリックされたIDを取得

Local Storageの配列内に該当IDがあれば削除、なければ追加(例: [123, 456] ➔ [456])。

2
2. ローカルストレージへデータを保存

最新の状態に更新された新しい配列 [456] をローカルストレージに保存。

3
3. syncUI()

通信を待たずに、画面内の該当する全てのボタンの見た目を即座にオフ(非活性)にする。

4
4. refreshFavWidget() 発火

UI同期後、最新リストを読み込むための関数を実行。

5
Ajax POST (fav_ids: [456])
6
6. ajax_refresh_fav_list() 発火

PHP側でリクエストを受信。SESSION情報をお気に入りの最新配列 [456] に更新。

7
7. ショートコードを再実行

最新のSESSIONを基に、ID: 456 のみが含まれる新しいお気に入りリストHTMLを生成。

8
HTML (Response) を返却
9
9. ウィジェットHTMLを差し替え

今削除した記事が消えた、1件減った状態の最新リストに丸ごと切り替わる。

10
10. syncUI()

新しく差し替わったウィジェット内のボタン状態を、Local Storageの内容と最終同期。

実装手順

以下のコードをfunctions.phpに追加します。

  • ラベル
    PHPを追加

    以下のコードをfunctions.phpに追加します。

    // フロント用JS・CSSを追加
    add_action('wp_enqueue_scripts', function () {
      $ajax_url = admin_url('admin-ajax.php');
    
      ob_start();
      ?>
      jQuery(function ($) {
        var KEY = 'my_fav_page_list';
    
        /**
         * ローカルストレージからIDリストを取得
         *
         * @return {Array<string|number>} 保存されているIDの配列
         */
        function getIdsFromLS() {
          try {
            return JSON.parse(localStorage.getItem(KEY) || '[]');
          } catch (e) {
            return [];
          }
        }
    
        /**
         * IDリストをローカルストレージに保存
         *
         * @param {Array<string|number>} ids 保存するIDの配列
         * @return {void}
         */
        function saveIds(ids) {
          localStorage.setItem(KEY, JSON.stringify(ids));
        }
    
        /**
         * お気に入りボタンの状態を更新
         *
         * @return {void}
         */
        function syncUI() {
          const currentIds = getIdsFromLS().map(String);
    
          $('.fav-button').each(function () {
            const id = String($(this).data('id'));
            $(this).toggleClass('active', currentIds.indexOf(id) > -1);
          });
        }
    
        /**
         * .favorite要素(Swiperコンテナ本体)に対してSwiperを初期化
         *
         * @param {jQuery} $container 初期化対象の要素
         * @return {void}
         */
        function initFavoriteSwiper($container) {
          if (typeof Swiper === 'undefined') {
            return;
          }
    
          if (!$container.length || !$container.hasClass('swiper')) {
            return;
          }
    
          // 初期化済みの場合は処理対象外
          if ($container[0].swiper) {
            return;
          }
    
          new Swiper($container[0], {
            slidesPerView: 'auto',
            spaceBetween: 30,
            navigation: {
              nextEl: $container.find('.swiper-button-next')[0],
              prevEl: $container.find('.swiper-button-prev')[0],
            },
            observer: true,
            observeParents: true,
          });
        }
    
        /**
         * お気に入り一覧をAjaxで更新
         *
         * @return {void}
         */
        function refreshFavWidget() {
          const $favoriteArea = $('.favorite');
    
          if (!$favoriteArea.length) {
            return;
          }
    
          const ids = getIdsFromLS();
    
          // ローカルストレージのIDをPOST送信
          $.post('<?php echo esc_url($ajax_url); ?>', {
            action: 'refresh_fav_list',
            fav_ids: ids
          }, function (response) {
            if (!response) {
              return;
            }
    
            // 差し替え前に、既存のSwiperインスタンスを破棄
            $favoriteArea.each(function () {
              if (this.swiper) {
                this.swiper.destroy(true, true);
              }
            });
    
            // DOMを差し替え
            const $newContent = $($.trim(response));
            $favoriteArea.replaceWith($newContent);
            syncUI();
    
            // 新しいコンテナに対してSwiperを再初期化
            initFavoriteSwiper($newContent);
          });
        }
    
        $(document).on('click', '.fav-button', function (e) {
          e.preventDefault();
          e.stopPropagation();
    
          const $btn = $(this);
          const id   = String($btn.data('id'));
          let ids    = getIdsFromLS().map(String);
          const index = ids.indexOf(id);
    
          // お気に入り状態を変更
          if (index > -1) {
            ids.splice(index, 1);
          } else {
            ids.push(id);
          }
    
          // ローカルストレージを更新
          saveIds(ids);
    
          syncUI();
          refreshFavWidget();
    
          // お気に入り一覧の更新を通知
          $(document).trigger('favListUpdated');
        });
    
        // お気に入り一覧更新時に再描画
        $(document).on('favListUpdated', function () {
          syncUI();
          refreshFavWidget();
        });
    
        refreshFavWidget();
        syncUI();
      });
      <?php
      $js = ob_get_clean();
    
      wp_add_inline_script('jquery', $js, 'after');
    });
    
    
    /**
     * Ajaxでお気に入り一覧を出力
     *
     * @return void
     */
    if (!function_exists('ajax_refresh_fav_list')):
    function ajax_refresh_fav_list() {
      // POSTされたお気に入りIDを取得
      $fav_ids = isset($_POST['fav_ids']) ? array_map('intval', (array) $_POST['fav_ids']) : [];
    
      if (!session_id() && !headers_sent()) {
        session_start();
      }
    
      $_SESSION['my_fav_page_list'] = $fav_ids;
    
      // お気に入り一覧を生成
      echo do_shortcode('[new_list action="favorite"]');
      wp_die();
    }
    endif;
    
    
    // Ajaxアクションを追加
    add_action('wp_ajax_refresh_fav_list', 'ajax_refresh_fav_list');
    add_action('wp_ajax_nopriv_refresh_fav_list', 'ajax_refresh_fav_list');
    
    
    // 新着記事のクエリを設定
    add_filter('widget_entries_args', function ($args) {
      if (empty($GLOBALS['favorite'])) {
        return $args;
      }
    
      $ids = [];
    
      // セッションからお気に入りIDを取得
      if (wp_doing_ajax() && isset($_SESSION['my_fav_page_list'])) {
        $ids = $_SESSION['my_fav_page_list'];
      }
    
      // お気に入り記事がない場合は終了
      if (empty($ids) || !is_array($ids)) {
        $args['post__in']       = [0];
        $args['posts_per_page'] = 0;
      } else {
        $ids               = array_unique($ids);
        $args['post__in']  = array_reverse($ids);
        $args['orderby']    = 'post__in';
      }
    
      $args['ignore_sticky_posts'] = true;
    
      return $args;
    });
    
    
    // テンプレート(エントリーカード)を変更
    add_filter('cocoon_part__tmp/entry-card', function ($content) {
      ob_start();
      get_fav_button();
      $button = ob_get_clean();
    
      return preg_replace('/(<h2[^>]*>)/', '$1' . $button, $content);
    });
    
    
    /**
     * お気に入りボタンを出力
     *
     * @return void
     */
    if (!function_exists('get_fav_button')):
    function get_fav_button() {
      ?>
      <span class="fav-button" data-id="<?= get_the_ID(); ?>" title="お気に入り"><i class="fas fa-paperclip"></i></span>
      <?php
    }
    endif;
    
    
    // 新着記事の属性を設定
    add_filter('shortcode_atts_generate_widget_entries_tag', function ($out, $pairs, $atts) {
      if (!session_id() && !headers_sent()) {
        session_start();
      }
    
      if (
        empty($GLOBALS['favorite']) &&
        (!isset($out['action']) || $out['action'] !== 'favorite')
      ) {
        return $out;
      }
    
      // 初回の設定をセッションに保存
      if (!wp_doing_ajax()) {
        $_SESSION['my_fav_page_list_atts'] = $out;
      }
      // Ajax再描画時に復元
      else if (isset($_SESSION['my_fav_page_list_atts'])) {
        $out = $_SESSION['my_fav_page_list_atts'];
      }
    
      $out['class'] .= ($out['class'] ? ' ' : '') . 'favorite';
    
      $GLOBALS['favorite'] = true;
      add_action('widget_entry_card_date_before', 'get_fav_button', 10, 2);
    
      // 新着記事終了時に状態をリセット
      add_filter('do_shortcode_tag', function ($output, $tag) {
        if ($tag === 'new_list') {
          $GLOBALS['favorite'] = false;
          remove_action('widget_entry_card_date_before', 'get_fav_button');
        }
    
        return $output;
      }, 10, 2);
    
      return $out;
    }, 10, 3);
    
    
    // ウィジェットに設定メニューを追加
    add_action('in_widget_form', function ($widget, $return, $instance) {
      // 対象ウィジェットを確認
      if ($widget->id_base !== 'new_entries') {
        return;
      }
    
      $f_id    = $widget->get_field_id('favorite');
      $f_name  = $widget->get_field_name('favorite');
      $checked = checked(!empty($instance['favorite']), true, false);
    
      ?>
      <p>
        <input type="checkbox" id="<?= $f_id; ?>" class="widefat" name="<?= $f_name; ?>" value="1" <?= $checked; ?>>
        <label for="<?= $f_id; ?>">お気に入り</label>
      </p>
      <?php
    }, 10, 3);
    
    
    // ウィジェットの入力データを更新
    add_filter('widget_update_callback', function ($instance, $new_instance) {
      $instance['favorite'] = isset($new_instance['favorite']) ? 1 : 0;
    
      return $instance;
    }, 10, 2);
    
    
    // ウィジェットの表示設定を変更
    add_filter('widget_display_callback', function ($instance, $widget, $args) {
      if ($widget->id_base == 'new_entries' && !empty($instance['favorite'])) {
        $GLOBALS['favorite'] = true;
        add_filter('widget_entries_args', 'clear_initial_widget_args', 999);
      } else {
        $GLOBALS['favorite'] = false;
      }
    
      return $instance;
    }, 10, 3);
    
    
    /**
     * 初回ページ読み込み時のお気に入りクエリを初期化
     *
     * @param array $args 現在のウィジェットのクエリ引数
     * @return array 更新されたクエリ引数
     */
    if (!function_exists('clear_initial_widget_args')):
    function clear_initial_widget_args($args) {
      if (!wp_doing_ajax()) {
        $args['post__in']       = [0];
        $args['posts_per_page'] = 0;
      }
    
      return $args;
    }
    endif;
  • ラベル
    「お気に入りをリセット」ボタンを追加

    以下のコードをfunctions.phpに追加します。

    add_shortcode('my_clear_fav_button', function () {
      ob_start();
      ?>
      <style>
      .clear-container {
        text-align: center;
      }
    
      /* 非活性 */
      .clear-fav-btn {
        background-color: var(--fav-button-off);
        border-color: var(--fav-button-off);
        pointer-events: none;
      }
    
      /* 活性 */
      .clear-fav-btn.active {
        background-color: var(--fav-button-on);
        border-color: var(--fav-button-on);
        cursor: pointer;
        pointer-events: auto;
      }
      </style>
    
      <script>
      jQuery(function ($) {
    
        /**
         * ローカルストレージからIDリストを取得
         *
         * @return {Array<string>} 保存されているIDの配列
         */
        function getIdsFromLS() {
          try {
            return JSON.parse(localStorage.getItem('my_fav_page_list') || '[]');
          } catch (e) {
            return [];
          }
        }
    
        /**
         * リセットボタンの状態を更新
         *
         * @return {void}
         */
        function syncClearButtonUI() {
          const currentIds = getIdsFromLS();
          $('.clear-fav-btn').toggleClass('active', currentIds.length > 0);
        }
    
        $(document).on('click', '.clear-fav-btn', function (e) {
          e.preventDefault();
    
          // ローカルストレージを削除
          localStorage.removeItem('my_fav_page_list');
    
          // ボタンの状態を更新
          syncClearButtonUI();
    
          // お気に入り一覧の更新を通知
          $(document).trigger('favListUpdated');
        });
    
        // お気に入り一覧更新時に状態を更新
        $(document).on('favListUpdated', function () {
          syncClearButtonUI();
        });
    
        // 初回の状態を確認
        syncClearButtonUI();
      });
      </script>
    
      <div class="clear-container">
        <button class="clear-fav-btn btn">お気に入りをリセット</button>
      </div>
      <?php
      return ob_get_clean();
    });

さいごに

今回の方法を使えば、プラグインを使わずに、独自の「お気に入り」機能を追加することができます。
「新着記事」ウィジェットに「お気に入り」オプションを設け、ユーザーは任意の投稿をお気に入りとして登録・表示できるようになります。
この仕組みにより、ユーザーが興味のあるコンテンツに簡単にアクセスでき、利便性を向上させることが可能です。

タイトルとURLをコピーしました