PluginProbe ʕ •ᴥ•ʔ
VK Blocks / trunk
VK Blocks vtrunk
1.123.0 1.122.0 1.121.0 1.120.0 1.119.2 1.119.1 1.119.0 1.118.7 1.82.0.0 1.82.0.1 1.83.0.0 1.83.0.1 1.84.0.0 1.84.0.1 1.85.0.2 1.85.0.3 1.85.1.0 1.85.1.1 1.86.0.0 1.86.0.1 1.86.1.0 1.87.0.0 1.87.0.1 1.88.0.1 1.88.0.2 1.89.0.0 1.9.2 1.90.0.0 1.90.0.1 1.90.1.1 1.91.0.0 1.91.1.1 1.92.0.0 1.92.0.1 1.92.1.0 1.92.1.1 1.93.0.0 trunk 1.93.0.1 0.1.1 1.93.1.0 0.10.3 1.93.1.1 0.11.0 1.94.0.0 0.12.6 1.94.0.1 0.15.1 1.94.1.0 0.16.2 1.94.2.1 0.17.6 1.94.2.2 0.2.2 1.95.0.3 0.22.4 1.95.0.4 0.26.7 1.96.2.0 0.3.2 1.96.2.1 0.31.0 1.97.0.1 0.35.5 1.97.0.2 0.37.5 1.99.0.0 0.38.8 1.99.0.1 0.39.4 0.4.7 0.41.0 0.42.0 0.44.13 0.45.2 0.46.1 0.47.0 0.48.0 0.49.8 0.5.2 0.50.3 0.51.0 0.52.2 0.53.2 0.54.0 0.55.0 0.56.2 0.56.3 0.57.6 0.58.10 0.59.0 0.6.0 0.60.1 0.61.2 0.7.1 0.8.2 1.0.16 1.10.0 1.100.0.0 1.100.0.1 1.102.0.0 1.102.0.1 1.103.0.0 1.104.0.0 1.104.0.1 1.105.1.0 1.105.1.1 1.106.0.0 1.106.0.1 1.107.0.0 1.107.0.1 1.107.0.2 1.108.0.0 1.108.0.1 1.109.0.0 1.109.0.1 1.11.4 1.110.0.0 1.110.0.1 1.111.0.0 1.111.0.1 1.111.0.2 1.112.0.0 1.112.0.1 1.113.0.0 1.113.0.1 1.114.0.0 1.114.0.1 1.114.2.0 1.114.2.1 1.115.0.0 1.115.0.1 1.115.1.0 1.115.1.1 1.115.2.0 1.115.2.1 1.116.0.0 1.116.0.1 1.116.1.0 1.116.1.1 1.116.2.0 1.117.0.0 1.117.0.1 1.117.1.0 1.118.2 1.118.5 1.12.0 1.13.2 1.14.1 1.15.1 1.16.11 1.17.0 1.18.6 1.19.0 1.19.1 1.2.3 1.20.7 1.21.0 1.22.4 1.23.0 1.24.5 1.25.1 1.26.2 1.27.7.2 1.28.0.1 1.29.2.0 1.3.9 1.30.0.1 1.31.0.1 1.32.0.2 1.33.2.1 1.36.1.5 1.37.0.0 1.39.2.1 1.4.6 1.40.0.1 1.40.1.0 1.40.1.1 1.41.0.0 1.41.0.1 1.41.2.2 1.41.2.3 1.43.0.0 1.43.0.1 1.43.0.2 1.44.0.0 1.44.0.1 1.45.0.0 1.45.0.1 1.46.0.0 1.46.0.1 1.47.0.0 1.47.0.1 1.47.1.0 1.48.0.0 1.48.0.1 1.48.0.2 1.48.1.0 1.48.1.1 1.5.0 1.50.0.0 1.50.0.1 1.51.0.0 1.51.0.1 1.52.0.0 1.52.0.1 1.53.0.0 1.53.0.1 1.54.0.0 1.54.0.1 1.55.0.0 1.55.0.1 1.56.0.0 1.56.0.1 1.57.0.0 1.57.0.2 1.57.0.3 1.57.0.4 1.57.0.5 1.57.1.0 1.57.1.1 1.57.1.2 1.58.0.0 1.58.0.1 1.59.0.0 1.59.0.1 1.6.0 1.60.0.0 1.60.0.1 1.63.0.0 1.63.0.1 1.64.0.0 1.64.0.1 1.64.1.0 1.64.1.2 1.67.0.0 1.67.0.1 1.68.0.0 1.68.0.1 1.69.0.0 1.69.0.1 1.69.1.1 1.69.1.2 1.7.1 1.70.0.0 1.70.0.1 1.71.0.0 1.71.0.1 1.72.0.0 1.72.1.0 1.72.1.1 1.73.0.0 1.73.0.1 1.74.0.0 1.74.0.1 1.75.0.0 1.75.1.0 1.75.1.1 1.76.0.0 1.76.0.1 1.76.1.0 1.76.1.1 1.76.2.0 1.76.2.1 1.77.0.0 1.77.0.1 1.78.0.0 1.78.0.1 1.79.0.0 1.79.0.1 1.79.0.2 1.79.0.3 1.79.1.0 1.79.1.1 1.8.2 1.80.1.0 1.80.1.1 1.80.1.2 1.81.0.1 1.81.0.2
vk-blocks / src / components / post-exclusion-control / index.js
vk-blocks / src / components / post-exclusion-control Last commit date
index.js 2 weeks ago
index.js
173 lines
1 import { __, sprintf } from '@wordpress/i18n';
2 import { BaseControl, FormTokenField } from '@wordpress/components';
3 import { useState, useMemo } from '@wordpress/element';
4 import { useSelect } from '@wordpress/data';
5 import { useDebounce } from '@wordpress/compose';
6 import { fixBrokenUnicode } from '@vkblocks/utils/fixBrokenUnicode';
7
8 /**
9 * 投稿IDで除外する投稿を選択するコンポーネント。
10 * 選択中の投稿タイプを対象にタイトル検索し、FormTokenField で除外する投稿を選択する。
11 * Component for selecting posts to exclude by post ID.
12 * Searches posts by title within the selected post types and lets the user pick posts to exclude via FormTokenField.
13 *
14 * @param {Object} props
15 * @param {Object} props.attributes ブロックの属性 / Block attributes.
16 * @param {Function} props.setAttributes 属性更新関数 / Setter for attributes.
17 * @param {string[]} props.selectedPostTypes 選択中の投稿タイプ slug の�
18 �列 / Array of selected post type slugs.
19 * @return {JSX.Element} FormTokenField
20 */
21 export function PostExclusionControl({
22 attributes,
23 setAttributes,
24 selectedPostTypes,
25 }) {
26 const { exclusionPosts } = attributes;
27
28 // 除外対象として保存済みの投稿ID�
29 �列 / Array of post ids saved as exclusion targets.
30 const selectedIds = useMemo(() => {
31 try {
32 const parsed = JSON.parse(fixBrokenUnicode(exclusionPosts || '[]'));
33 return Array.isArray(parsed) ? parsed : [];
34 } catch (e) {
35 return [];
36 }
37 }, [exclusionPosts]);
38
39 // 検索キーワード(FormTokenField への�
40 �力値)/ Search keyword ( input value of FormTokenField ).
41 const [search, setSearch] = useState('');
42 // �
43 �力のたびに getEntityRecords が発火しないよう、検索キーワードの更新をデバウンスする。
44 // Debounce the search keyword update so getEntityRecords does not fire on every keystroke.
45 const debouncedSetSearch = useDebounce(setSearch, 300);
46
47 // 検索対象の投稿タイプ(未選択時は post を既定とする)/ Post types to search ( fall back to post when none selected ).
48 const postTypes = useMemo(() => {
49 return selectedPostTypes && selectedPostTypes.length
50 ? selectedPostTypes
51 : ['post'];
52 }, [selectedPostTypes]);
53
54 // 検索キーワードに一致する投稿(候補表示用)/ Posts matching the search keyword ( for suggestions ).
55 const suggestionPosts = useSelect(
56 (select) => {
57 const { getEntityRecords } = select('core');
58 let results = [];
59 postTypes.forEach((postType) => {
60 const records = getEntityRecords('postType', postType, {
61 per_page: 20,
62 search: search || undefined,
63 _fields: 'id,title',
64 });
65 if (records) {
66 results = results.concat(records);
67 }
68 });
69 return results;
70 },
71 [search, postTypes]
72 );
73
74 // 保存済み投稿IDからトークン表示用のタイトルを解決 / Resolve titles for selected ids to display as tokens.
75 const selectedPosts = useSelect(
76 (select) => {
77 if (!selectedIds.length) {
78 return [];
79 }
80 const { getEntityRecords } = select('core');
81 let results = [];
82 // WP REST API の per_page 上限(100)を�
83 えるとリクエストが弾かれ
84 // タイトルが解決できなくなるため、選択IDを100件ずつに分割して取得する。
85 // Split selected ids into chunks of 100 to respect the WP REST API
86 // per_page cap ( requests over the cap fail and titles go unresolved ).
87 const chunkSize = 100;
88 const idChunks = [];
89 for (let i = 0; i < selectedIds.length; i += chunkSize) {
90 idChunks.push(selectedIds.slice(i, i + chunkSize));
91 }
92 postTypes.forEach((postType) => {
93 idChunks.forEach((chunk) => {
94 const records = getEntityRecords('postType', postType, {
95 per_page: chunk.length,
96 include: chunk,
97 _fields: 'id,title',
98 });
99 if (records) {
100 results = results.concat(records);
101 }
102 });
103 });
104 return results;
105 },
106 [selectedIds, postTypes]
107 );
108
109 // 投稿をトークンのラベル文字列に変換(タイトル重複に備えて #ID を付与)/ Convert a post into a token label ( append #ID to avoid duplicate titles ).
110 const makeLabel = (post) => {
111 const title =
112 post.title && post.title.rendered
113 ? post.title.rendered
114 : __('(no title)', 'vk-blocks');
115 return `${title} (#${post.id})`;
116 };
117
118 // ラベル ⇔ ID の対応表を作成 / Build label <-> id maps.
119 const labelToId = {};
120 const idToLabel = {};
121 [...suggestionPosts, ...selectedPosts].forEach((post) => {
122 const label = makeLabel(post);
123 labelToId[label] = post.id;
124 idToLabel[post.id] = label;
125 });
126
127 // 表示用トークン(タイトル未解決の場合は #ID で表示)/ Tokens to display ( fall back to #ID when the title is not resolved yet ).
128 const value = selectedIds.map((id) => idToLabel[id] || `#${id}`);
129 const suggestions = suggestionPosts.map(makeLabel);
130
131 return (
132 // BaseControl でラップして他の設定�
133 目と余白を揃え、検索対象の補足説明(help)を表示する。
134 // FormTokenField 自体は help プロップ非対応のため、help は BaseControl 側に持たせる。
135 // Wrap with BaseControl to match the spacing of other controls and to show the search-target help text.
136 // FormTokenField itself does not support the help prop, so help is placed on BaseControl.
137 <BaseControl
138 id="vk_postList-excludePosts"
139 help={sprintf(
140 // translators: search target post types
141 __('Search target: %s', 'vk-blocks'),
142 postTypes.join(', ')
143 )}
144 >
145 <FormTokenField
146 label={__('Exclude by Article', 'vk-blocks')}
147 value={value}
148 suggestions={suggestions}
149 onInputChange={(input) => debouncedSetSearch(input)}
150 onChange={(tokens) => {
151 const ids = tokens
152 .map((token) => {
153 // 候補から選択された場合はラベルからIDを引く / Look up the id from the label when picked from suggestions.
154 if (labelToId[token] !== undefined) {
155 return labelToId[token];
156 }
157 // 既存トークン(#ID 形式)はIDを抽出 / Extract the id from an existing token ( #ID format ).
158 const matched = String(token).match(/#(\d+)/);
159 if (matched) {
160 return parseInt(matched[1], 10);
161 }
162 return null;
163 })
164 .filter((id) => id !== null);
165 setAttributes({ exclusionPosts: JSON.stringify(ids) });
166 }}
167 __experimentalExpandOnFocus
168 __experimentalShowHowTo={false}
169 />
170 </BaseControl>
171 );
172 }
173