0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

wordpressのカテゴリーで全てにチェックすると全部のカテゴリーにチェックが入る

0
Last updated at Posted at 2025-12-15

wordpressのカテゴリーで全てというのを作って、チェックをした時に全てのカテゴリーにチェックが入るようにしたかった。
jsで書いてみたけれど、要素がnullになって、焦った。

クラシックエディタと、Gutenbergで調べてみた。

待てば出るバージョン(クラシックエディタ)

(function () {
  function initCategoryAll() {
    const list = document.getElementById('categorychecklist');
    if (!list) return false;

    // すでに「すべて」作ってたら終了
    if (!document.getElementById('in-category-all')) {
      const li = document.createElement('li');
      li.innerHTML = `
        <label>
          <input type="checkbox" id="in-category-all">
          <strong>すべて</strong>
        </label>
      `;
      list.prepend(li);
    }

    const all = document.getElementById('in-category-all');
    const cats = Array.from(list.querySelectorAll('input[type="checkbox"]'))
      .filter(el => el !== all);

    all.addEventListener('change', function () {
      const checked = this.checked;
      cats.forEach(cat => { cat.checked = checked; });
    });

    cats.forEach(cat => {
      cat.addEventListener('change', function () {
        all.checked = cats.every(c => c.checked);
      });
    });

    return true;
  }

  // すでにあれば即初期化
  if (initCategoryAll()) return;

  // なければDOM追加を監視して、出てきた瞬間に初期化
  const obs = new MutationObserver(() => {
    if (initCategoryAll()) obs.disconnect();
  });

  obs.observe(document.documentElement, { childList: true, subtree: true });
})();

Gutenberg

Gutenberg だと DOM(#categorychecklist とか)を待っても出てこないので、null 問題はほぼ確定で起きます。

以下、「すべて」チェックをGutenberg右サイドに追加して、チェックで全カテゴリー付与する完成形を貼ります(DOMに触りません)。

**右サイドバーの「カテゴリー」パネル(ブロックエディタ標準UI)**の場合、
• あのパネル自体は Reactで描画されていて
• #categorychecklist みたいなULは存在しない(または安定しない)ので
• 同じ親要素に

を挿入する方式は効きません(前に起きた「青いけど保存されない」系のズレが出やすい)

✅ 正攻法は Gutenbergの仕組み(wp.plugins / wp.data)で、サイドバーに“同列のUI”を追加することです。

正攻法:サイドバーに「すべて / 解除」UIを追加(保存も必須/最大も効く)

  1. functions.php(依存を増やして読み込み)

add_action('admin_enqueue_scripts', function($hook) {
  if (!in_array($hook, ['post.php', 'post-new.php'], true)) return;

  wp_enqueue_script(
    'my-gb-cat-safe',
    get_stylesheet_directory_uri() . '/assets/js/gb-cat-safe.js',
    ['wp-element','wp-components','wp-data','wp-plugins','wp-edit-post','wp-api-fetch','wp-notices'],
    '1.0.0',
    true
  );

  wp_add_inline_script('my-gb-cat-safe', 'window.MY_GB_CAT = ' . wp_json_encode([
    'max'      => 3,      // 最大数(0/nullなら無制限)
    'required' => true,   // 必須
    'labels'   => [
      'panel' => 'カテゴリー',
      'all'   => 'すべて',
      'req'   => 'カテゴリーを最低1つ選択してください。',
      'max'   => 'カテゴリーは最大 %d 個まで選択できます。',
      'allOn' => '全選択',
      'allOff'=> '全解除',
    ],
  ]) . ';', 'before');
});

  1. assets/js/gb-cat-ui.js(UI + ロジック)

(() => {
  const cfg = window.MY_GB_CAT || {};
  const max = cfg.max;                 // 0/null -> 無制限
  const required = !!cfg.required;

  const labels = cfg.labels || {};
  const PANEL_TITLE = labels.panel || 'カテゴリー';
  const LABEL_ALL = labels.all || 'すべて';
  const MSG_REQ = labels.req || 'カテゴリーを最低1つ選択してください。';
  const MSG_MAX_TPL = labels.max || 'カテゴリーは最大 %d 個まで選択できます。';
  const BTN_ALL_ON = labels.allOn || '全選択';
  const BTN_ALL_OFF = labels.allOff || '全解除';

  const LOCK_KEY = 'my-gb-cat-lock';
  const msgMax = (n) => MSG_MAX_TPL.replace('%d', String(n));

  const { createElement: el, useEffect, useMemo, useRef, useState } = wp.element;
  const { CheckboxControl, Button, Notice } = wp.components;
  const { PluginDocumentSettingPanel } = wp.editPost;
  const { useSelect, useDispatch } = wp.data;

  async function fetchAllCategoryIds() {
    // 100件超ある場合はページングが必要(ひとまず100件想定)
    const terms = await wp.apiFetch({ path: '/wp/v2/categories?per_page=100&hide_empty=0' });
    return (terms || []).map(t => t.id);
  }

  function Panel() {
    // Gutenbergの「今の投稿に紐づくカテゴリID配列」
    const categories = useSelect((select) => {
      return select('core/editor').getEditedPostAttribute('categories') || [];
    }, []);

    const categoriesKey = useMemo(
      () => categories.slice().sort((a,b)=>a-b).join(','),
      [categories]
    );

    const { editPost, lockPostSaving, unlockPostSaving } = useDispatch('core/editor');
    const { createNotice, removeNotice } = useDispatch('core/notices');

    // ループ防止用(effect内の“自分の変更”を区別)
    const internalRef = useRef(false);
    const lockedRef = useRef(null);
    const lastMaxNoticeKeyRef = useRef('');

    // 「すべて」チェックの見た目:
    // 厳密に「全カテゴリ選択時だけON」にするには全IDが必要で重いので、
    // ここでは「1つ以上選択されていればON」=“まとめ操作スイッチ”として扱う(安定優先)
    const allCheckedUI = categories.length > 0;

    const overMax = !!max && categories.length > max;
    const needReq = required && categories.length === 0;

    // 必須/最大に応じて保存ロック(状態が変わった時だけ実行)
    useEffect(() => {
      if (internalRef.current) return;

      const shouldLock = needReq || overMax;
      if (lockedRef.current === shouldLock) return; // 変化なしなら何もしない
      lockedRef.current = shouldLock;

      if (shouldLock) lockPostSaving(LOCK_KEY);
      else unlockPostSaving(LOCK_KEY);
    }, [categoriesKey, needReq, overMax, lockPostSaving, unlockPostSaving]);

    // 最大超過していたら自動で戻す(最後尾を削る)
    useEffect(() => {
      if (!max) return;
      if (internalRef.current) return;
      if (categories.length <= max) return;

      internalRef.current = true;
      try {
        const next = categories.slice(0, max);
        editPost({ categories: next });

        // noticeは連打しない
        const nk = `${categoriesKey}->${next.slice().sort((a,b)=>a-b).join(',')}`;
        if (lastMaxNoticeKeyRef.current !== nk) {
          lastMaxNoticeKeyRef.current = nk;
          createNotice('error', msgMax(max), { id: 'my-gb-cat-max', isDismissible: true });
        }
      } finally {
        // 次のtickで解除(同一レンダー内の連鎖を避ける)
        setTimeout(() => { internalRef.current = false; }, 0);
      }
    }, [categoriesKey, categories, editPost, createNotice]);

    async function onToggleAll(nextChecked) {
      internalRef.current = true;
      try {
        if (nextChecked) {
          const allIds = await fetchAllCategoryIds();
          let next = allIds;
          if (max && allIds.length > max) next = allIds.slice(0, max);
          editPost({ categories: next });
        } else {
          editPost({ categories: [] });
        }
      } finally {
        setTimeout(() => { internalRef.current = false; }, 0);
      }
    }

    return el(
      PluginDocumentSettingPanel,
      { name: 'my-cat-safe-panel', title: PANEL_TITLE, className: 'my-cat-safe-panel' },

      // 「カテゴリーの一項目風」チェック
      el(CheckboxControl, {
        label: LABEL_ALL,
        checked: allCheckedUI,
        onChange: onToggleAll,
        help: max ? `(最大 ${max} 個まで)` : undefined,
      }),

      // ついでにボタンも付けたい場合(任意)
      el('div', { style: { display: 'flex', gap: '8px', marginTop: '6px' } },
        el(Button, { variant: 'secondary', onClick: () => onToggleAll(true) }, BTN_ALL_ON),
        el(Button, { variant: 'secondary', onClick: () => onToggleAll(false) }, BTN_ALL_OFF),
      ),

      needReq && el(Notice, { status: 'warning', isDismissible: false }, MSG_REQ),
      overMax && el(Notice, { status: 'error', isDismissible: false }, msgMax(max))
    );
  }

  wp.plugins.registerPlugin('my-gb-cat-safe-plugin', { render: Panel });
})();

右サイドバーに 「カテゴリー操作」 というパネルが追加されて、そこで
• すべて選択
• すべて解除
• 必須/最大の警告
• 保存ロック(公開/更新できない)

が正しく効きます。
(React側の状態を操作してるので、“青いだけ”問題は出ません)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?