プラグインなしでカテゴリーを好きな順番に並び替える方法

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

拡張:管理画面からカテゴリーをドラッグ&ドロップで並び替えする方

独自の設定画面を設け、プラグイン「Category Order and Taxonomy Terms Order」のように、ドラッグ&ドロップでカテゴリーの順番を並び替えられるようにします。

完成イメージ

実装手順

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

// 管理画面にメニューを追加
add_action('admin_menu', function () {
  add_submenu_page(
    'edit.php',
    'カテゴリー並び替え',
    'カテゴリー並び替え',
    'manage_categories',
    'category_order',
    'render_category_order'
  );
});

/**
 * 管理画面ページを出力
 *
 * @return void
 */
if (!function_exists('render_category_order')):
function render_category_order() {
  // カテゴリー情報を取得
  $terms = get_terms(['taxonomy' => 'category', 'hide_empty' => false]);

  // 取得エラー時は空配列を設定
  if (is_wp_error($terms)) {
    $terms = [];
  }

  // 保存済みの順序とカテゴリー名で並び順を変更
  usort($terms, function ($a, $b) {
    return ((int) get_term_meta($a->term_id, 'my_category_order', true) <=> (int) get_term_meta($b->term_id, 'my_category_order', true))
      ?: strcmp($a->name, $b->name);
  });

  // 親カテゴリーごとの分類情報を初期化
  $map = [];

  // 親カテゴリーIDをキーにしてカテゴリー情報を登録
  foreach ($terms as $t) {
    $map[$t->parent][] = $t;
  }

  // 階層ごとのカテゴリーを再帰的に表示する処理を設定
  $print = function ($pid, $d) use (&$print, $map) {
    // 指定された親カテゴリーに属するカテゴリーを取得
    foreach ($map[$pid] ?? [] as $t) {
      // 表示用のカテゴリーIDを取得
      $term_id = (int) $t->term_id;

      // 表示用の階層を取得
      $depth = (int) $d;

      // 階層に応じた左余白を設定
      $margin_left = $depth * 30;

      // カテゴリー名をエスケープして取得
      $term_name = esc_html($t->name);

      // カテゴリーID・階層・左余白を設定
      echo '<li data-id="' . $term_id . '" data-depth="' . $depth . '" style="margin-left:' . $margin_left . 'px">';
      echo '<span class="category-order__grip" aria-hidden="true">⋮⋮</span>';
      echo '<span class="category-order__title">' . $term_name . '</span>';
      echo '<small class="category-order__depth">depth: <b>' . $depth . '</b></small>';
      echo '</li>';

      // 子カテゴリーを次の階層で表示する処理を設定
      $print($t->term_id, $d + 1);
    }
  };

  ?>
  <div class="wrap">
    <h1>カテゴリー 階層・並び順</h1>
    <p>上下ドラッグで順序、左右ドラッグで階層を変更できます。子カテゴリーも一緒に移動します。</p>

    <ul id="category-order-list"><?php $print(0, 0); ?></ul>

    <p>
      <button id="category-order-save" class="button button-primary">設定を保存</button>
      <span id="category-order-msg"></span>
    </p>
  </div>
  <?php
}
endif;

// 管理画面用JS・CSSを追加
add_action('admin_enqueue_scripts', function () {
  if (($_GET['page'] ?? '') !== 'category_order') {
    return;
  }

  ob_start();
  ?>
  #category-order-list {
    background-color: #fff;
    border: 1px solid #ccd0d4;
    border-radius: 4px;
    list-style: none;
    margin-top: 20px;
    max-width: 700px;
    padding: 10px;
  }

  #category-order-list li {
    align-items: center;
    background-color: #f6f7f7;
    border: 1px solid #c3c4c7;
    border-radius: 3px;
    box-sizing: border-box;
    cursor: move;
    display: flex;
    font-weight: bold;
    margin-bottom: 8px;
    min-height: 39px;
    padding: 8px 12px;
  }

  #category-order-list li:last-child {
    margin-bottom: 0;
  }

  #category-order-list li:hover {
    background-color: #f0f0f1;
  }

  .category-order__grip {
    color: #8c8f94;
    flex-shrink: 0;
    font-weight: normal;
    letter-spacing: -1px;
    margin-right: 8px;
    user-select: none;
  }

  .category-order__title {
    font-weight: normal;
    overflow-wrap: anywhere;
  }

  .category-order__depth {
    color: #787c82;
    flex-shrink: 0;
    font-size: 12px;
    font-weight: normal;
    margin-left: auto;
    padding-left: 12px;
  }

  #category-order-list li.category-order__placeholder {
    background-color: #e5f5fa;
    border: 1px dashed #2271b1;
    border-radius: 3px;
    box-sizing: border-box;
    height: 39px;
    margin-bottom: 8px;
    padding: 0;
  }

  #category-order-list li.ui-sortable-helper {
    background-color: #fff;
    box-shadow: 0 2px 8px rgba(0, 0, 0, .2);
  }
  <?php
  wp_add_inline_style('wp-admin', ob_get_clean());

  ob_start();
  ?>
  jQuery(function ($) {
    // ドラッグ幅・対象リスト・移動情報・子カテゴリーを設定
    var I = 30, $l = $("#category-order-list"), x0, d0, C = $();

    // カテゴリーの階層を取得
    var D = function ($i) {
      return +$i.attr("data-depth") || 0;
    };

    // カテゴリーの階層と表示位置を変更
    var S = function ($i, d) {
      $i.attr("data-depth", d).css("margin-left", d * I);
      $i.find("b").text(d);
    };

    // カテゴリーリストのドラッグ操作を設定
    $l.sortable({
      // ドラッグ中のプレースホルダーと判定方法を設定
      placeholder: "category-order__placeholder",
      tolerance: "pointer",

      // ドラッグ開始時の階層と子カテゴリーを取得
      start: function (e, ui) {
        // ドラッグ開始位置と対象カテゴリーの階層を取得
        x0 = e.pageX;
        d0 = D(ui.item);

        // 移動対象の子カテゴリーを初期化
        C = $();

        // 対象カテゴリーに属する子孫カテゴリーを取得
        ui.item.nextAll("li").not(ui.placeholder).each(function () {
          // 同じ階層以下のカテゴリーで取得を終了
          if (D($(this)) <= d0) return false;

          // 移動対象の子孫カテゴリーを登録
          C = C.add(this);
        });

        // 子孫カテゴリーを一時的にリストから切り離す
        C.detach();

        // ドラッグ位置の再計算を設定
        $l.sortable("refreshPositions");
      },

      // ドラッグ中の移動先階層を変更
      sort: function (e, ui) {
        // プレースホルダーの直前にあるカテゴリーを取得
        var p = ui.placeholder.prevAll("li").not(ui.item).first();

        // 横方向の移動量から階層を計算して変更
        var d = Math.max(0, Math.min(d0 + Math.round((e.pageX - x0) / I), p.length ? D(p) + 1 : 0));

        // 移動先の階層を一時保存
        ui.item.data("nd", d);

        // プレースホルダーの表示位置を変更
        ui.placeholder.css("margin-left", d * I);
      },

      // ドラッグ終了時に階層と子カテゴリーの位置を変更
      stop: function (e, ui) {
        // 移動先の直前にあるカテゴリーを取得
        var p = ui.item.prev();

        // ドラッグ中に設定した階層を取得
        var d = ui.item.data("nd");

        // 移動先の階層を有効な範囲に変更
        d = Math.max(0, Math.min(d === undefined ? d0 : d, p.length ? D(p) + 1 : 0));

        // 移動したカテゴリーの階層を変更
        S(ui.item, d);

        // 子孫カテゴリーの階層を親カテゴリーに合わせて変更
        C.each(function () {
          S($(this), D($(this)) + d - d0);
        });

        // 子孫カテゴリーを親カテゴリーの後ろに登録
        ui.item.after(C);

        // 子孫カテゴリーの一時情報を初期化
        C = $();

        // 階層の連続性を確認するための初期値を設定
        var prev = -1;

        // 各カテゴリーの階層を確認して変更
        $l.children().each(function () {
          // 直前のカテゴリーより深い階層になりすぎないように変更
          var n = Math.min(D($(this)), prev + 1);

          // 必要な場合は階層を変更
          if (n !== D($(this))) {
            S($(this), n);
          }

          // 次のカテゴリーの比較用階層を設定
          prev = n;
        });
      }
    });

    // 保存ボタンのクリック処理を設定
    $("#category-order-save").on("click", function () {
      // 親カテゴリー情報と保存対象を初期化
      var s = [], items = [];

      // 画面上の順序からカテゴリーIDと親IDを取得
      $l.children().each(function () {
        // 現在の階層とカテゴリーIDを取得
        var d = Math.min(D($(this)), s.length);
        var id = $(this).data("id");

        // 現在の階層に合わせて親情報を設定
        s.length = d;

        // カテゴリーIDと親カテゴリーIDを登録
        items.push([id, s[d - 1] || 0]);

        // 次のカテゴリーの親情報としてカテゴリーIDを登録
        s.push(id);
      });

      // AJAXで保存処理を実行
      $.post(ajaxurl, {
        // 保存処理のアクションを設定
        action: "category_order_save",

        // カテゴリー情報をJSON形式で登録
        items: JSON.stringify(items)
      })
      .done(function (r) {
        // 保存成功時は画面を再読み込み
        if (r.success) {
          location.reload();
        } else {
          // 保存失敗時のメッセージを設定
          $("#category-order-msg").text("保存に失敗しました。");
        }
      })
      .fail(function () {
        // 通信失敗時のメッセージを設定
        $("#category-order-msg").text("通信エラー");
      });
    });
  });
  <?php

  wp_enqueue_script('jquery-ui-sortable');
  wp_add_inline_script('jquery-ui-sortable', ob_get_clean());
});

// タームのSQL句を並び順に合わせて変更
add_filter('terms_clauses', function ($c, $tax, $args) {
  global $wpdb;

  // 並び順の指定を取得
  $o = $args['orderby'] ?? 'name';

  // 対象タクソノミー以外、または対象の並び順以外は処理対象外
  if (!in_array('category', (array) $tax, true) || $o !== 'name' || (is_admin() && isset($_GET['orderby']))) {
    return $c;
  }

  // 並び順用タームメタとの結合条件を置換
  $c['join'] .= " LEFT JOIN {$wpdb->termmeta} AS co ON co.term_id = t.term_id AND co.meta_key = 'my_category_order'";

  // 並び順をタームメタとカテゴリー名の順に置換
  $c['orderby'] = 'ORDER BY CAST(co.meta_value AS SIGNED), t.name';

  // 既定の並び順指定を解除
  $c['order'] = '';

  // 変更後のSQL句を返却
  return $c;
}, 10, 3);

// Ajaxアクションを追加
add_action('wp_ajax_category_order_save', function () {
  // 権限チェック
  if (!current_user_can('manage_categories')) {
    wp_send_json_error();
  }

  // 送信されたカテゴリー情報をJSONから取得
  $items = json_decode(wp_unslash($_POST['items'] ?? ''), true);

  // 配列形式でない場合は処理対象外
  if (!is_array($items)) {
    wp_send_json_error();
  }

  // 更新件数とエラー件数を初期化
  $n = $err = 0;

  // 送信されたカテゴリー情報を順番に処理
  foreach (array_values($items) as $order => $it) {
    // カテゴリーIDを取得
    $id = (int) ($it[0] ?? 0);

    // 親カテゴリーIDを取得
    $parent = (int) ($it[1] ?? 0);

    // ターム情報を取得
    $t = get_term($id, 'category');

    // タームが存在しない場合や編集権限がない場合は処理対象外
    if (!$t || is_wp_error($t) || !current_user_can('edit_term', $id)) {
      continue;
    }

    // 親カテゴリーが存在しない場合はエラー件数を登録
    if ($parent && !term_exists($parent, 'category')) {
      $err++;
      continue;
    }

    // 自分自身を親にする場合はエラー件数を登録
    if ($parent === $id) {
      $err++;
      continue;
    }

    // 変更状態の初期値を設定
    $changed = false;

    // 親カテゴリーが変更されている場合は親情報を変更
    if ((int) $t->parent !== $parent) {
      // 親カテゴリーIDを変更
      $result = wp_update_term($id, 'category', ['parent' => $parent]);

      // 親カテゴリーの変更に失敗した場合はエラー件数を登録
      if (is_wp_error($result)) {
        $err++;
        continue;
      }

      // 変更済みの状態を設定
      $changed = true;
    }

    // 保存済みの並び順を取得
    $old_order = get_term_meta($id, 'my_category_order', true);

    // 並び順が変更されている場合は並び順を登録
    if ((string) $old_order !== (string) $order) {
      // 並び順をタームメタに登録
      update_term_meta($id, 'my_category_order', (string) $order);

      // 変更済みの状態を設定
      $changed = true;
    }

    // 変更があった場合は更新件数を登録
    if ($changed) {
      $n++;
    }
  }

  // タクソノミーのキャッシュをクリアして処理を終了
  clean_taxonomy_cache('category');

  // 保存結果の通知内容を設定
  if (!$err) {
    // 成功通知を一時データに登録
    set_transient('category_order_admin_notice', [
      'type' => 'success',
      'message' => '設定を保存しました。',
    ], 30);
  } else {
    // 警告通知を一時データに登録
    set_transient('category_order_admin_notice', [
      'type' => 'warning',
      'message' => '一部のカテゴリーを保存できませんでした。',
    ], 30);
  }

  // 更新件数とエラー件数をJSON形式で返却して処理を終了
  wp_send_json_success(['n' => $n, 'err' => $err]);
});

// 通知メッセージを出力
add_action('admin_notices', function () {
  if ($notice = get_transient('category_order_admin_notice')) {
    echo '<div class="notice notice-' . esc_attr($notice['type']) . ' is-dismissible"><p>' . esc_html($notice['message']) . '</p></div>';
    delete_transient('category_order_admin_notice');
  }
});

さいごに

今回紹介した方法を使えば、プラグインなしでカテゴリーを好きな順番に並び替えることができます。
ネット上にはさまざまなコード例がありますが、get_terms()のorderbyにincludeを指定することで、ID順に並び替えることが可能です。
これにより、複雑なコードを書く必要がなく実現可能です。

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