管理画面UIの仕組みを理解してCSSのみでスライダー制御する方法

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

はじめに

以下のコードでは、タブ切り替えやスライダー制御をJavaScriptで行っていますが、これらをCSSのみで制御する構成へ改善します。

この記事では、この方法について説明します。

その過程で、以下の管理画面UI実装ポイントを学習します。

  • ドラッグ&ドロップによる並び替え
  • 画像の複数選択による一括登録
  • 不要画像の削除による管理性向上

完成イメージ

実装手順

プラグインの形式で実装します。

以下の手順で実装します。

  • my-slider
    • my-slider.php
    • uninstall.php
    • assets
      • css
        • admin.css
        • frontend.css
      • js
        • admin.js
    • includes
      • admin.php
      • frontend.php
  • ラベル
    PHPを追加

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

    <?php
    /**
     * Plugin Name: My Slider
     * Description: 管理画面から画像を登録できる無限ループスライダーです。
     * Version: 1.0.0
     * Author: CHU-YA
     */
    
    if (!defined('ABSPATH')) {
      exit;
    }
    
    // 定数定義
    define('SLIDER_PATH', plugin_dir_path(__FILE__));
    define('SLIDER_URL', plugin_dir_url(__FILE__));
    define('SLIDER_COUNT', 5);
    
    
    // ファイル読み込み
    require_once SLIDER_PATH . 'includes/admin.php';
    require_once SLIDER_PATH . 'includes/frontend.php';
  • ラベル
    PHPを追加

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

    <?php
    
    if (!defined('WP_UNINSTALL_PLUGIN')) {
      exit;
    }
    
    delete_option('my_slider');
  • ラベル
    PHPを追加

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

    <?php
    if (!defined('ABSPATH')) {
      exit;
    }
    
    // 管理画面にメニューを追加
    add_action('admin_menu', function () {
      add_menu_page(
        'my_slider',
        'My Slider',
        'manage_options',
        'slider',
        'render_slider',
        'dashicons-format-image',
        80
      );
    });
    
    
    /**
     * 管理画面ページを出力
     *
     * @return void
     */
    if (!function_exists('render_slider')):
    function render_slider() {
      $settings = get_option('my_slider', []);
      $h = isset($settings['height']) ? $settings['height'] : '300';
      $s_val = isset($settings['speed']) ? $settings['speed'] : '5';
      $all_data = isset($settings['sliders']) ? $settings['sliders'] : [];
    
      // 保存後に表示するタブ
      $active_tab = isset($_POST['slider_tab'])
        ? max(1, min(SLIDER_COUNT, (int) $_POST['slider_tab']))
        : 1;
    
      ?>
      <div class="wrap slider-admin">
    
        <h1>スライダー設定</h1>
    
        <form method="post">
          <?php wp_nonce_field('slider_action', 'slider_nonce'); ?>
    
          <div class="slider-admin__settings">
    
            <!-- スライダーの高さ -->
            <div class="slider-admin__settings-item">
              <label>高さ:</label>
              <input type="number" name="slider_height" value="<?php echo esc_attr($h); ?>">
              <span>px</span>
            </div>
    
            <!-- スライダーの表示速度 -->
            <div class="slider-admin__settings-item">
              <label>1枚あたりの速度:</label>
              <input type="number" step="0.5" name="slider_speed" value="<?php echo esc_attr($s_val); ?>">
              <span>秒</span>
            </div>
    
          </div>
    
          <!-- タブ切り替え用ラジオボタン -->
          <?php for ($s = 1; $s <= SLIDER_COUNT; $s++): ?>
            <input type="radio" name="slider_tab" id="tab<?php echo $s; ?>" class="slider-admin__tab-input" value="<?php echo $s; ?>" <?php checked($s, $active_tab); ?>>
          <?php endfor; ?>
    
          <!-- タブのナビゲーション -->
          <ul class="slider-admin__tabs">
            <?php for ($s = 1; $s <= SLIDER_COUNT; $s++): ?>
              <li>
                <label for="tab<?php echo $s; ?>" class="slider-admin__tab-link">
                  スライダー <?php echo $s; ?>
                </label>
              </li>
            <?php endfor; ?>
          </ul>
    
          <!-- 各スライダーの設定コンテンツ -->
          <?php for ($s = 1; $s <= SLIDER_COUNT; $s++): ?>
            <div id="content<?php echo $s; ?>" class="slider-admin__tab-content">
    
              <!-- スライダー用ショートコード -->
              <div class="slider-admin__shortcode">
                <code>[my_slider id="<?php echo $s; ?>"]</code>
              </div>
    
              <!-- メディアライブラリから画像を追加 -->
              <button type="button" class="button button-primary slider-admin__add" data-slider="<?php echo $s; ?>">
                画像を追加
              </button>
    
              <!-- 登録画像のギャラリー -->
              <div class="slider-admin__gallery sortable" id="gallery_<?php echo $s; ?>">
    
                <?php
                // このスライダーに登録されている画像IDを取得
                $ids = isset($all_data[$s]) ? $all_data[$s] : [];
    
                foreach ($ids as $id):
                  $url = wp_get_attachment_image_url($id, 'medium') ?: wp_get_attachment_image_url($id, 'full');
                ?>
    
                  <!-- ギャラリーアイテム -->
                  <div class="slider-admin__gallery-item">
                    <img src="<?php echo esc_url($url); ?>">
                    <input type="hidden" name="slider_<?php echo $s; ?>_ids[]" value="<?php echo (int) $id; ?>">
                    <!-- 画像削除ボタン -->
                    <span class="slider-admin__remove">&times;</span>
                  </div>
    
                <?php endforeach; ?>
    
              </div>
            </div>
          <?php endfor; ?>
    
          <!-- 設定保存ボタン -->
          <p>
            <input type="submit" name="slider_save" class="button button-primary button-large" value="設定を保存">
          </p>
    
        </form>
      </div>
    
      <?php
    }
    endif;
    
    
    // 設定を保存
    add_action('admin_init', function () {
      if ('POST' !== $_SERVER['REQUEST_METHOD'] || !isset($_POST['slider_nonce'])) {
        return;
      }
    
      // nonceチェック
      check_admin_referer('slider_action', 'slider_nonce');
    
      $new_settings = [];
      $new_settings['height'] = (int) $_POST['slider_height'];
      $new_settings['speed'] = (float) $_POST['slider_speed'];
    
      $sliders_data = [];
    
      for ($s = 1; $s <= SLIDER_COUNT; $s++) {
        $ids = isset($_POST["slider_{$s}_ids"])
          ? array_map('intval', $_POST["slider_{$s}_ids"])
          : [];
    
        $sliders_data[$s] = array_values(array_filter($ids));
      }
    
      $new_settings['sliders'] = $sliders_data;
    
      update_option('my_slider', $new_settings);
    
      // 成功通知を一時保存
      set_transient('my_admin_notice', [
        'type'    => 'success',
        'message' => '設定を保存しました。',
      ], 30);
    });
    
    
    // 通知メッセージを出力
    add_action('admin_notices', function () {
      if ($notice = get_transient('my_admin_notice')) {
        echo '<div class="notice notice-' . esc_attr($notice['type']) . ' is-dismissible"><p>' . esc_html($notice['message']) . '</p></div>';
        delete_transient('my_admin_notice');
      }
    });
    
    
    // 管理画面用JS・CSSを追加
    add_action('admin_enqueue_scripts', function () {
      if (($_GET['page'] ?? '') !== 'slider') {
        return;
      }
    
      wp_enqueue_media();
      wp_enqueue_script('jquery-ui-sortable');
    
      wp_enqueue_style('slider-admin', SLIDER_URL . 'assets/css/admin.css');
      wp_enqueue_script('slider-admin', SLIDER_URL . 'assets/js/admin.js', ['jquery', 'jquery-ui-sortable'], null, true);
    });
  • ラベル
    PHPを追加

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

    <?php
    if (!defined('ABSPATH')) {
      exit;
    }
    
    // ショートコードを追加
    add_shortcode('my_slider', function ($atts) {
      $atts = shortcode_atts([
        'id' => '1',
      ], $atts);
    
      $settings = get_option('my_slider', []);
      $h = isset($settings['height']) ? (int) $settings['height'] : 300;
      $speed = isset($settings['speed']) ? (float) $settings['speed'] : 5.0;
      $sliders = isset($settings['sliders']) ? $settings['sliders'] : [];
      $ids = isset($sliders[(int) $atts['id']]) ? $sliders[(int) $atts['id']] : [];
    
      if (empty($ids)) {
        return '';
      }
    
      $total_duration = count($ids) * $speed;
      $unique_id = 'slider-' . uniqid();
      $list = array_merge($ids, $ids, $ids);
    
      ob_start();
      ?>
      <div class="slider" id="<?= esc_attr($unique_id); ?>" style="--slider-height: <?= esc_attr($h); ?>px; --slider-duration: <?= esc_attr($total_duration); ?>s;">
        <div class="slider__track">
          <?php
          foreach ($list as $id):
            $url = wp_get_attachment_image_url($id, 'full');
    
            if (!$url) {
              continue;
            }
          ?>
            <img src="<?= esc_url($url); ?>" alt="">
          <?php endforeach; ?>
        </div>
      </div>
      <?php
      return ob_get_clean();
    });
    
    
    // フロント用JS・CSSを追加
    add_action('wp_enqueue_scripts', function () {
      wp_enqueue_style('slider-frontend', SLIDER_URL . 'assets/css/frontend.css');
    });
  • ラベル
    JavaScriptを追加

    以下のコードをadmin.jsに追加します。

    jQuery(function ($) {
    
      // タブを切り替える
      $('.slider-admin__tab-input').on('change', function () {
        var tabId = $(this).attr('id').replace('tab', '');
    
        // コンテンツを切り替える
        $('.slider-admin__tab-content').hide();
        $('#content' + tabId).show();
    
        // アクティブ状態を切り替える
        $('.slider-admin__tab-link').removeClass('is-active');
        $('.slider-admin__tab-link[for="tab' + tabId + '"]').addClass('is-active');
      });
    
      // 選択中のタブを初期表示
      $('.slider-admin__tab-input:checked').trigger('change');
    
    
      // 画像を並べ替える
      $('.sortable').sortable({
        placeholder: 'ui-state-highlight',
        tolerance: 'pointer',
        forcePlaceholderSize: true,
      });
    
    
      // 画像追加ボタンを処理
      $('.slider-admin__add').on('click', function (e) {
        e.preventDefault();
    
        // スライダー番号を取得
        var sliderId = $(this).data('slider');
        var existingIds = [];
    
        // 既存画像IDを取得
        $('#gallery_' + sliderId + ' input[name="slider_' + sliderId + '_ids[]"]').each(function () {
          existingIds.push($(this).val());
        });
    
        // メディアフレームを作成
        var custom_uploader = wp.media({
          title: '画像を選択',
          button: {
            text: 'スライダーに追加'
          },
          multiple: 'add',
        });
    
        // 既存画像を選択状態に設定
        custom_uploader.on('open', function () {
          var selection = custom_uploader.state().get('selection');
    
          existingIds.forEach(function (id) {
            var attachment = wp.media.attachment(id);
    
            attachment.fetch().then(function () {
              selection.add(attachment);
            });
          });
        });
    
        // 選択画像を表示
        custom_uploader.on('select', function () {
          var selection = custom_uploader.state().get('selection');
    
          // ギャラリーを初期化
          $('#gallery_' + sliderId).empty();
    
          // 選択画像を追加
          selection.each(function (attachment) {
            var attr = attachment.attributes;
    
            // 画像URLを取得
            var imgUrl =
              (attr.sizes && attr.sizes.medium)
                ? attr.sizes.medium.url
                : attr.url;
    
            // 画像HTMLを作成
            var html =
              '<div class="slider-admin__gallery-item">' +
                '<img src="' + imgUrl + '">' +
                '<input type="hidden" name="slider_' + sliderId + '_ids[]" value="' + attr.id + '">' +
                '<span class="slider-admin__remove">&times;</span>' +
              '</div>';
    
            // ギャラリーに追加
            $('#gallery_' + sliderId).append(html);
          });
        });
    
        // メディアフレームを表示
        custom_uploader.open();
      });
    
    
      // 画像を削除
      $(document).on('click', '.slider-admin__remove', function () {
        $(this).closest('.slider-admin__gallery-item').remove();
      });
    
    });
  • ラベル
    CSSを追加

    以下のコードをadmin.cssに追加します。

    :root {
      --img-width: 100px;
      --tab-color: #0071a1;
    }
    
    /* 設定画面全体のコンテナ */
    .slider-admin {
      font-family: sans-serif;
      margin-top: 20px;
      max-width: 1000px;
    }
    
    /* 高さ・速度設定の共通エリア */
    .slider-admin__settings {
      align-items: center;
      background-color: #fff;
      border: 1px solid #ccc;
      display: flex;
      gap: 20px;
      margin-bottom: 20px;
      padding: 20px;
    }
    
    /* 数値入力フォーム */
    .slider-admin__settings-item input[type='number'] {
      margin: 0;
      width: 80px;
    }
    
    /* ショートコード */
    .slider-admin__shortcode {
      background-color: #f0f6fb;
      border-left: 4px solid var(--tab-color);
      margin-bottom: 20px;
      padding: 12px;
    }
    
    /* タブ */
    .slider-admin__tabs {
      border-bottom: 2px solid var(--tab-color);
      display: flex;
      gap: 4px;
      list-style: none;
      margin: 20px 0 0 0;
      padding: 0;
    }
    
    /* タブ切り替え用ラジオボタン */
    .slider-admin__tab-input {
      display: none !important;
    }
    
    /* タブのラベル */
    .slider-admin__tab-link {
      background-color: #dcdcdc;
      border: 1px solid #ccc;
      border-bottom: none;
      border-radius: 4px 4px 0 0;
      cursor: pointer;
      display: inline-block;
      padding: 10px 20px;
    }
    
    /* アクティブなタブ */
    .slider-admin__tab-link.is-active {
      background-color: var(--tab-color);
      border-color: var(--tab-color);
      color: #fff;
      font-weight: bold;
    }
    
    /* タブリスト */
    .slider-admin__tabs li {
      margin: 0;
    }
    
    /* タブコンテンツ */
    .slider-admin__tab-content {
      background-color: #fff;
      border: 1px solid #ccc;
      border-top: 0;
      display: none;
      min-height: 250px;
      padding: 20px;
    }
    
    
    /* ギャラリー */
    .slider-admin__gallery {
      background-color: #f7f7f7;
      border: 2px dashed #dcdcdc;
      display: grid;
      gap: 15px;
      grid-template-columns: repeat(auto-fill, var(--img-width));
      margin-top: 20px;
      min-height: var(--img-width);
      padding: 15px;
    }
    
    /* ギャラリーアイテム */
    .slider-admin__gallery-item {
      background-color: #fff;
      border: 1px solid #dcdcdc;
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
      cursor: move;
      height: var(--img-width);
      position: relative;
      width: var(--img-width);
    }
    
    
    /* ギャラリー画像 */
    .slider-admin__gallery-item img {
      display: block;
      height: 100%;
      object-fit: cover;
      pointer-events: none;
      width: 100%;
    }
    
    
    /* ドラッグガイド */
    .ui-state-highlight {
      background-color: #e2eaf2;
      border: 1px dashed var(--tab-color);
      height: var(--img-width);
      visibility: visible;
      width: var(--img-width);
    }
    
    /* アイテム削除ボタン */
    .slider-admin__remove {
      background-color: #d63638;
      border-radius: 50%;
      color: #fff;
      cursor: pointer;
      display: block;
      font-size: 16px;
      font-weight: bold;
      line-height: 20px;
      position: absolute;
      right: -10px;
      text-align: center;
      top: -10px;
      width: 20px;
      z-index: 10;
    }
  • ラベル
    CSSを追加

    以下のコードをfrontend.cssに追加します。

    /* スライダー全体 */
    .slider {
      height: var(--slider-height, 300px);
      overflow: hidden;
      position: relative;
      width: 100%;
    }
    
    /* スライダートラック */
    .slider__track {
      animation-duration: var(--slider-duration, 5s);
      animation-iteration-count: infinite;
      animation-name: slider_loop;
      animation-timing-function: linear;
      display: flex;
      height: 100%;
      width: max-content;
    }
    
    /* マウスオーバー時に停止 */
    .slider__track:hover {
      animation-play-state: paused;
    }
    
    @keyframes slider_loop {
      0% {
        transform: translateX(0);
      }
    
      100% {
        transform: translateX(calc(-100% / 3));
      }
    }

仕組みと設計の解説

並び替え機能(sortable)とは

sortableは jQuery UI ライブラリに含まれる機能で、HTML要素をドラッグ&ドロップで並び替え可能にするUIコンポーネントです。
WordPress管理画面では jquery-ui-sortableが標準で利用できるため、追加ライブラリなしで安全に導入できるのが大きなメリットです。

設計上のポイント

各画像アイテムの中に hidden inputを配置しておくことで、画面上の並び順そのものがそのまま保存順として扱えるようになります。
sortableはDOM構造を書き換えるだけなので、画像を並び替えるとhidden inputの並び順も同時に変わり、その状態のままフォーム送信されます。
そのため、JavaScript側で別途配列を管理する必要はなく、PHP側でも$_POST['slider_1_ids']をそのまま保存するだけで正しい順序を保持できます。

複数画像選択(wp.media)とは

wp.mediaを使った複数画像選択の定型コードは次の形になります。

var frame = wp.media({
  title: '画像を選択',
  button: { text: 'スライダーに追加' },
  multiple: 'add'
});

frame.on('select', function () {
  var selection = frame.state().get('selection');
  selection.each(function(attachment) {
    var attr = attachment.attributes;
    console.log(attr.id, attr.url);
  });
});

frame.open();

このコードは、WordPressのメディアライブラリを開き、ユーザーが選択した複数画像を取得するためのほぼ完全な定型パターンです。
実務では、この形をベースにしてHTML生成や保存処理を追加していきます。

処理の流れと動作説明

wp.media({...})は、WordPress内部のメディア選択UIコンポーネントを生成します。
この時点ではまだ画面には表示されず、タイトル文言やボタンラベル、複数選択の可否といった設定だけが行われます。

続いてframe.open()によってメディアモーダルが表示されます。

ユーザーが確定ボタンを押すと、frame.on('select', ...)が発火し、frame.state().get('selection')によって現在選択されているメディア一覧が取得されます。
各要素はattachment modelとして扱われ、idやurlなどの情報を取り出して、HTML生成やhidden inputの作成といったアプリケーション固有の処理を行います。

設計上のポイント

UIと選択管理は wp.media に完全に任せ、取得・表示・保存だけを自分のコードで実装するという責務分離が、wp.mediaを使った複数画像の選択設計となります(表2参照)。

wp.media による処理分担構造
処理範囲担当
メディアUI表示wp.media
チェック管理wp.media
複数選択制御wp.media
選択結果取得JavaScript側
DOM反映JavaScript側
データベース保存PHP側

画像削除の仕組み

削除ボタンをクリックすると、画面上の要素(DOM)だけが削除されます。
同時にhidden inputも消えるため、フォーム送信時のPOSTデータにはその画像IDが含まれなくなります。
その結果、サーバー側では該当データを受信せず、データベース保存時にも自動的に削除された状態として扱われます。

uninstall.phpの役割

uninstall.phpは、プラグインが「完全に削除」された瞬間にのみ実行される専用ファイルです(表1参照)。

実行されるタイミング
操作uninstall.php
無効化実行されない
ファイル削除(FTPなど)実行されない
プラグイン削除実行される


主な目的は、プラグインが保存した各種データを WordPress環境から完全に削除することにあります。

これらを削除しないままにすると、プラグインは存在しないのにデータベースだけが残る状態となり、環境の保守性や可読性を損ないます。

例えば、対象となるデータには以下が含まれます。

  • optionsテーブルの設定値
  • カスタムテーブル
  • 一時キャッシュ

さいごに

今回紹介した方法を使えば、JavaScriptに依存せず、CSSだけでタブ切り替えやスライダー制御が可能なWordPress管理画面UIを構築できます。
sortableやwp.mediaといったWordPress標準機能と組み合わせることで、操作性と保守性の両立も実現でき、プラグイン開発の品質向上につながります。

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