itmar-block-packages
Version:
Common React components for WordPress custom blocks, now fully TypeScript-ready.
503 lines (499 loc) • 26.6 kB
JavaScript
;
var jsxRuntime = require('react/jsx-runtime');
var element = require('@wordpress/element');
var i18n = require('@wordpress/i18n');
var _ = require('lodash');
var components = require('@wordpress/components');
var apiFetch = require('@wordpress/api-fetch');
var url = require('@wordpress/url');
//const _ = require("lodash");
const restFetchData = async (path) => {
try {
// apiFetch にジェネリクス <T> を渡すことで戻り値に型が付く
const ret_data = await apiFetch({ path });
return ret_data;
}
catch (error) {
// エラーメッセージの安全な取り出し
const errorMessage = error.message || "Unknown error occurred";
console.error("Error fetching data:", errorMessage);
// エラー時は undefined を返して呼び出し元で判定できるようにする
return undefined;
}
};
//コンボボックスコントロールのレンダリング関数
const SelectControl = (props) => {
const { selectedSlug, label, homeUrl, fetchOptions } = props;
const [options, setOptions] = element.useState([]);
element.useEffect(() => {
const fetchData = async () => {
try {
const fetchedOptions = await fetchOptions(homeUrl, props);
setOptions(fetchedOptions);
}
catch (error) {
console.error("Error fetching data:", error.message);
}
};
fetchData();
}, [
fetchOptions,
homeUrl,
props.restBase,
props.status,
props.perPage,
props.orderby,
props.order,
props.search,
]);
const selectedInfo = options.find((info) => info.slug === selectedSlug);
return (jsxRuntime.jsx(components.ComboboxControl, { label: label, options: options, value: selectedInfo ? selectedInfo.value : undefined, onChange: (newValue) => {
const newInfo = options.find((info) => info.value === newValue);
props.onChange(newInfo);
} }));
};
//選択コントロールのレンダリング関数
const ChoiceControl = (props) => {
const { selectedSlug, choiceItems, dispTaxonomies, type, blockMap, fetchFunction, } = props;
const [choices, setChoices] = element.useState([]);
element.useEffect(() => {
if (!selectedSlug)
return; //ポストタイプのスラッグが選択されていないときは処理終了
const fetchData = async () => {
try {
const fetchChoices = await fetchFunction(selectedSlug);
// ここでチェックを入れる
if (!fetchChoices) {
setChoices([]); // データがない場合は空配列にする
return;
}
setChoices(fetchChoices);
//指定の投稿タイプに含まれないフィールドを削除する
pruneChoiceItemsByObjectKeys(fetchChoices[0], choiceItems);
}
catch (error) {
console.error("Error fetching data:", error.message);
}
};
fetchData();
}, [selectedSlug, fetchFunction]);
//選択肢が変わったときに選択されている項目の配列内容を更新するハンドラ
const handleChoiceChange = (checked, target, setItems) => {
if (checked) {
// targetが重複していない場合のみ追加
if (!setItems.some((item) => _.isEqual(item, target))) {
return [...setItems, target];
}
}
else {
// targetを配列から削除
return setItems.filter((item) => !_.isEqual(item, target));
}
return setItems;
};
/**
* dataObj のキー一覧を「choiceItems と比較する形」に変換して Set で返す
* - 通常キー: そのまま
* - acf / meta: 子キーに `${parent}_` を付けたもの(例: acf_relate_url, meta_footnotes)
*/
function buildComparableKeySet(dataObj) {
const keySet = new Set();
const addNestedKeys = (obj, prefix) => {
if (!obj || typeof obj !== "object" || Array.isArray(obj))
return;
for (const [key, val] of Object.entries(obj)) {
const fieldKey = `${prefix}.${key}`;
keySet.add(fieldKey);
if (val && typeof val === "object" && !Array.isArray(val)) {
addNestedKeys(val, fieldKey);
}
}
};
if (!dataObj || typeof dataObj !== "object")
return keySet;
for (const [key, val] of Object.entries(dataObj)) {
if ((key === "acf" || key === "meta") &&
val &&
typeof val === "object" &&
!Array.isArray(val)) {
for (const [childKey, childVal] of Object.entries(val)) {
const baseKey = `${key}_${childKey}`;
keySet.add(baseKey);
if (childVal &&
typeof childVal === "object" &&
!Array.isArray(childVal)) {
addNestedKeys(childVal, baseKey);
}
}
continue;
}
keySet.add(key);
}
return keySet;
}
/**
* choiceItems を dataObj のキーに合わせて削除する
* - choiceItems が string 配列でも、{value: "..."} の配列でも動くようにしてあります
*/
function pruneChoiceItemsByObjectKeys(dataObj, items) {
const validKeys = buildComparableKeySet(dataObj);
const getItemKey = (item) => {
if (typeof item === "string")
return item;
if (item && typeof item === "object")
return item.value ?? item.key ?? item.name ?? "";
return "";
};
const next = (items ?? []).filter((item) => validKeys.has(getItemKey(item)));
// ★ 配列の参照はそのまま、中身だけ置き換える
items.splice(0, items.length, ...next);
return items; // 必要なら返す
}
//階層化されたカスタムフィールドのフィールド名を表示する関数
const dispCustumFields = (obj, prefix = "", isImage = false) => {
return Object.entries(obj).map(([key, value]) => {
const fieldName = prefix ? `${prefix}.${key}` : key; //prefixはグループ名
const fieldLabel = key.replace(/^(meta_|acf_)/, "");
//オブジェクトであって配列でないものがグループと考える
if (typeof value === "object" &&
!Array.isArray(value) &&
value !== null) {
return (jsxRuntime.jsxs("div", { className: "group_area", children: [jsxRuntime.jsx("div", { className: "group_label", children: fieldLabel }), jsxRuntime.jsx("div", { className: "field_group", children: dispCustumFields(value, fieldName, isImage) })] }, fieldName));
}
else {
if (key === "meta__acf_changed" || key === "meta_footnotes")
return null; //_acf_changedは対象外
//フィールドを表示するブロックの選択肢
const options = [
{ value: "itmar/design-title", label: "itmar/design-title" },
{ value: "core/paragraph", label: "core/paragraph" },
{ value: "core/image", label: "core/image" },
{ value: "itmar/slide-mv", label: "itmar/slide-mv" },
];
return (jsxRuntime.jsxs("div", { className: "itmar_custom_field_set", children: [jsxRuntime.jsx(components.ToggleControl, { className: "field_choice", label: fieldLabel, checked: choiceItems.some((choiceField) => choiceField === fieldName), onChange: (checked) => {
const newItems = handleChoiceChange(checked, fieldName, choiceItems);
props.onChange(newItems);
} }), !isImage && (jsxRuntime.jsx(components.ComboboxControl, { options: options, value: blockMap[fieldName] || "itmar/design-title", onChange: (newValue) => {
props.onBlockMapChange({
...blockMap,
[fieldName]: newValue || "",
});
} }))] }, fieldName));
}
});
};
return (jsxRuntime.jsxs("div", { className: `${type}_section`, children: [type === "taxonomy" &&
choices.map((choice, index) => (jsxRuntime.jsxs("div", { className: "term_section", children: [jsxRuntime.jsxs("div", { className: "tax_label", children: [choice.name, jsxRuntime.jsx(components.ToggleControl, { label: i18n.__("Display", "block-collections"), checked: dispTaxonomies.some((tax) => tax === choice.slug), onChange: (checked) => {
const newTax = handleChoiceChange(checked, choice.slug, dispTaxonomies);
props.onSetDispTax(newTax);
} })] }), choice.terms?.map((term, tIndex) => (jsxRuntime.jsx(components.CheckboxControl, { label: term.name, checked: choiceItems.some((c) => c.taxonomy === choice.slug && c.term.id === term.id), onChange: (checked) => {
const target = {
taxonomy: choice.slug,
term: { id: term.id, slug: term.slug, name: term.name },
};
props.onChange(handleChoiceChange(checked, target, choiceItems));
} }, tIndex)))] }, index))), type === "field" &&
choices.map((choice, index) => {
//metaの対象カスタムフィールドが含まれるかのフラグ
const metaFlg = choice.meta &&
!Object.keys(choice.meta).every((key) => key === "_acf_changed" || key === "footnotes");
//acfの対象カスタムフィールドが含まれるかのフラグ
const acfFlg = choice.acf &&
typeof choice.acf === "object" &&
!Array.isArray(choice.acf);
return (jsxRuntime.jsxs("div", { className: "field_section", children: [choice.title && (jsxRuntime.jsx(components.ToggleControl, { className: "field_choice", label: i18n.__("Title", "block-collections"), checked: choiceItems.some((choiceField) => choiceField === "title"), onChange: (checked) => {
const newChoiceFields = handleChoiceChange(checked, "title", choiceItems);
props.onChange(newChoiceFields);
} })), choice.content && (jsxRuntime.jsx(components.ToggleControl, { className: "field_choice", label: i18n.__("Content", "block-collections"), checked: choiceItems.some((choiceField) => choiceField === "content"), onChange: (checked) => {
const newChoiceFields = handleChoiceChange(checked, "content", choiceItems);
props.onChange(newChoiceFields);
} })), choice.date && (jsxRuntime.jsx(components.ToggleControl, { className: "field_choice", label: i18n.__("Date", "block-collections"), checked: choiceItems.some((choiceField) => choiceField === "date"), onChange: (checked) => {
const newChoiceFields = handleChoiceChange(checked, "date", choiceItems);
props.onChange(newChoiceFields);
} })), choice.excerpt && (jsxRuntime.jsx(components.ToggleControl, { className: "field_choice", label: i18n.__("Excerpt", "block-collections"), checked: choiceItems.some((choiceField) => choiceField === "excerpt"), onChange: (checked) => {
const newChoiceFields = handleChoiceChange(checked, "excerpt", choiceItems);
props.onChange(newChoiceFields);
} })), (choice.featured_media || choice.featured_media === 0) && (jsxRuntime.jsx(components.ToggleControl, { className: "field_choice", label: i18n.__("Featured Image", "block-collections"), checked: choiceItems.some((choiceField) => choiceField === "featured_media"), onChange: (checked) => {
const newChoiceFields = handleChoiceChange(checked, "featured_media", choiceItems);
props.onChange(newChoiceFields);
} })), choice.link && (jsxRuntime.jsxs("div", { className: "itmar_custom_field_set", children: [jsxRuntime.jsx(components.ToggleControl, { className: "field_choice", label: i18n.__("Single Page Link", "block-collections"), checked: choiceItems.some((choiceField) => choiceField === "link"), onChange: (checked) => {
const newChoiceFields = handleChoiceChange(checked, "link", choiceItems);
props.onChange(newChoiceFields);
} }), jsxRuntime.jsx(components.ComboboxControl, { options: [
{
value: "itmar/design-button",
label: "itmar/design-button",
},
{
value: "itmar/design-title",
label: "itmar/design-title",
},
], value: blockMap["link"], onChange: (newValue) => {
const newBlockMap = {
...blockMap,
link: newValue ?? "",
};
props.onBlockMapChange(newBlockMap);
} }), jsxRuntime.jsx("p", { children: i18n.__("If no block is specified, a link will be set to the parent block, Design Group.", "block-collections") })] })), (metaFlg || acfFlg) && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "custom_field_label", children: i18n.__("Custom Field", "block-collections") }), jsxRuntime.jsx("div", { className: "custom_field_area", children: dispCustumFields({
// meta はそのまま
...Object.entries(choice.meta).reduce((acc, [key, value]) => ({
...acc,
[`meta_${key}`]: value,
}), {}),
// acf は「同名で _source があるもののベース側を除く」
...Object.entries(choice.acf)
.filter(([key]) => !key.endsWith("_source"))
.reduce((acc, [key, value]) => ({
...acc,
[`acf_${key}`]: value,
}), {}),
}) })] }))] }, index));
}), type === "imgField" &&
choices.map((choice, index) => {
//metaの対象カスタムフィールドが含まれるかのフラグ
const metaFlg = choice.meta &&
!Object.keys(choice.meta).every((key) => key === "_acf_changed" || key === "footnotes");
//acfの対象カスタムフィールドが含まれるかのフラグ
const acfFlg = choice.acf &&
typeof choice.acf === "object" &&
!Array.isArray(choice.acf);
return (jsxRuntime.jsxs("div", { className: "field_section", children: [choice.content && (jsxRuntime.jsx(components.ToggleControl, { className: "field_choice", label: i18n.__("Content", "block-collections"), checked: choiceItems.some((choiceField) => choiceField === "content"), onChange: (checked) => {
const newChoiceFields = handleChoiceChange(checked, "content", choiceItems);
props.onChange(newChoiceFields);
} })), (choice.featured_media || choice.featured_media === 0) && (jsxRuntime.jsx(components.ToggleControl, { className: "field_choice", label: i18n.__("Featured Image", "block-collections"), checked: choiceItems.some((choiceField) => choiceField === "featured_media"), onChange: (checked) => {
const newChoiceFields = handleChoiceChange(checked, "featured_media", choiceItems);
props.onChange(newChoiceFields);
} })), (metaFlg || acfFlg) && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "custom_field_label", children: i18n.__("Custom Field", "block-collections") }), jsxRuntime.jsx("div", { className: "custom_field_area", children: dispCustumFields({
// meta はそのまま
...Object.entries(choice.meta).reduce((acc, [key, value]) => ({
...acc,
[`meta_${key}`]: value,
}), {}),
// acf は「同名で _source があるもののベース側を除く」
...Object.entries(choice.acf)
.filter(([key]) => !key.endsWith("_source"))
.reduce((acc, [key, value]) => ({
...acc,
[`acf_${key}`]: value,
}), {}),
}, "", true) })] }))] }, index));
})] }));
};
const fetchPagesOptions = async (home_url) => {
const allPages = [];
let page = 1;
while (true) {
// apiFetchに型を指定
const items = await apiFetch({
path: url.addQueryArgs("/wp/v2/pages", {
status: "publish", // 公開のみ
per_page: 100, // 最大100
page,
// orderby: "title",
// order: "asc",
}),
});
allPages.push(...items);
// 100件未満ならこれが最後
if (!items || items.length < 100)
break;
page++;
}
// ページIDが-1である要素をホーム要素として作成
if (!allPages.some((p) => p.id === -1)) {
allPages.unshift({
id: -1,
title: { rendered: "ホーム" },
link: home_url,
slug: "",
});
}
// 最終的な整形
const ret_pages = allPages.map((p) => ({
// ComboboxControl のために string に変換
value: String(p.id),
slug: p.slug,
label: p.title?.rendered ?? "",
link: p.link || (p.slug ? `${home_url}/${p.slug}` : home_url),
}));
return ret_pages;
};
const fetchArchiveOptions = async (home_url) => {
// apiFetchの戻り値は、キーが投稿タイプ名のオブジェクト
const response = await apiFetch({
path: "/wp/v2/types",
});
let idCounter = 0;
return Object.keys(response).reduce((acc, key) => {
const postType = response[key];
// アーカイブが有効なものだけを抽出
if (postType.has_archive === true) {
acc.push({
value: String(idCounter++),
slug: postType.slug,
rest_base: postType.rest_base,
link: `${home_url}/${postType.slug}`,
label: postType.name,
});
}
else if (typeof postType.has_archive === "string") {
//アーカイブ名がついているとき
acc.push({
value: String(idCounter++),
slug: postType.slug,
rest_base: postType.rest_base,
link: `${home_url}/${postType.has_archive}`,
label: postType.name,
});
}
return acc;
}, []);
};
/**
* 1. 投稿データ取得RestAPI関数
*/
const fetchPostOptions = async (home_url, props = {}) => {
const perPage = Math.min(Number(props.perPage || 100), 100);
const status = props.status || "publish"; // 管理画面で下書き等も含めたいなら 'any' を検討
const orderby = props.orderby || "title";
const order = props.order || "asc";
const search = props.search ? String(props.search) : "";
const restBase = props.restBase || "";
const allPosts = [];
const maxPagesSafety = 50; // 念のための安全上限
for (let page = 1; page <= maxPagesSafety; page++) {
const params = new URLSearchParams();
params.set("per_page", String(perPage));
params.set("page", String(page));
params.set("status", status);
params.set("orderby", orderby);
params.set("order", order);
// 必要なフィールドだけ返して軽量化
params.set("_fields", "id,slug,title,link");
if (search)
params.set("search", search);
const path = `/wp/v2/${encodeURIComponent(restBase)}?${params.toString()}`;
const posts = await apiFetch({ path });
if (!Array.isArray(posts) || posts.length === 0)
break;
allPosts.push(...posts);
// これ以上ページがない(=最終ページ)と判断
if (posts.length < perPage)
break;
}
// title.rendered はHTMLを含むことがあるのでタグを除去
const stripTags = (html) => String(html || "")
.replace(/<[^>]*>/g, "")
.trim();
return allPosts.map((post) => {
const title = stripTags(post?.title?.rendered) || "(no title)";
return {
value: String(post.id), // 重要: stringに変換
label: title,
slug: post.slug,
link: post.link || `${home_url}/?p=${post.id}`,
rest_base: restBase,
post_id: Number(post.id),
};
});
};
/**
* 2. タクソノミー取得RestAPI関数
*/
const restTaxonomies = async (post_type) => {
if (!post_type)
return;
const response = await apiFetch({
path: `/wp/v2/types/${post_type}?context=edit`,
});
const taxonomies = response.taxonomies;
const taxonomyPromises = taxonomies.map(async (slug) => {
const taxonomyResponse = await apiFetch({
path: `/wp/v2/taxonomies/${slug}?context=edit`,
});
const terms = await apiFetch({
path: `/wp/v2/${taxonomyResponse.rest_base}`,
});
return {
slug: slug,
name: taxonomyResponse.name,
rest_base: taxonomyResponse.rest_base,
terms: terms,
};
});
return await Promise.all(taxonomyPromises);
};
const termToDispObj = (terms, connectString) => {
const result = terms.reduce((acc, item) => {
const taxonomy = item.taxonomy;
const termName = item.term.name;
// taxonomyがまだ存在しない場合は初期化
if (!acc[taxonomy]) {
acc[taxonomy] = [];
}
// term.nameを配列に追加
acc[taxonomy].push(termName);
return acc;
}, {});
// 各taxonomyの配列を connectString でつなげて文字列化
const displayObj = {};
for (const taxonomy in result) {
displayObj[taxonomy] = result[taxonomy].join(connectString);
}
return displayObj;
};
/**
* 4. フィールド情報取得RestAPI関数
*/
const restFieldes = async (rest_base) => {
const selectedFields = [
"title",
"content",
"date",
"excerpt",
"featured_media",
"link",
"meta",
"acf",
];
const fieldsParam = selectedFields.join(",");
const ret_data = await apiFetch({
path: `/wp/v2/${rest_base}?_fields=${fieldsParam}&per_page=1&order=desc`,
});
return ret_data;
};
// --- 各コンポーネントの型定義 ---
/**
* 固定ページ選択用
*/
const PageSelectControl = (props) => jsxRuntime.jsx(SelectControl, { ...props, fetchOptions: fetchPagesOptions });
/**
* アーカイブ(投稿タイプ)選択用
*/
const ArchiveSelectControl = (props) => jsxRuntime.jsx(SelectControl, { ...props, fetchOptions: fetchArchiveOptions });
/**
* 投稿(各投稿タイプの中身)選択用
*/
const PostSelectControl = (props) => jsxRuntime.jsx(SelectControl, { ...props, fetchOptions: fetchPostOptions });
/**
* タクソノミー・ターム選択用
*/
const TermChoiceControl = (props) => jsxRuntime.jsx(ChoiceControl, { ...props, fetchFunction: restTaxonomies });
/**
* カスタムフィールド(ACF/Meta)選択用
*/
const FieldChoiceControl = (props) => (jsxRuntime.jsx(ChoiceControl, { ...props, type: "field" // フィールド用であることを固定
, fetchFunction: restFieldes }));
exports.ArchiveSelectControl = ArchiveSelectControl;
exports.FieldChoiceControl = FieldChoiceControl;
exports.PageSelectControl = PageSelectControl;
exports.PostSelectControl = PostSelectControl;
exports.TermChoiceControl = TermChoiceControl;
exports.fetchArchiveOptions = fetchArchiveOptions;
exports.fetchPagesOptions = fetchPagesOptions;
exports.fetchPostOptions = fetchPostOptions;
exports.restFetchData = restFetchData;
exports.restFieldes = restFieldes;
exports.restTaxonomies = restTaxonomies;
exports.termToDispObj = termToDispObj;
//# sourceMappingURL=wordpressApi.js.map