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を追加(保存も必須/最大も効く)
- 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');
});
⸻
- 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側の状態を操作してるので、“青いだけ”問題は出ません)
⸻