style.js
429 lines
| 1 | /** |
| 2 | * WordPress dependencies |
| 3 | */ |
| 4 | import { addFilter } from '@wordpress/hooks'; |
| 5 | import { createHigherOrderComponent } from '@wordpress/compose'; |
| 6 | import { InspectorControls } from '@wordpress/block-editor'; |
| 7 | import { |
| 8 | PanelBody, |
| 9 | SelectControl, |
| 10 | ToggleControl, |
| 11 | Notice, |
| 12 | } from '@wordpress/components'; |
| 13 | import { registerBlockVariation } from '@wordpress/blocks'; |
| 14 | import { __ } from '@wordpress/i18n'; |
| 15 | import { speak } from '@wordpress/a11y'; |
| 16 | import apiFetch from '@wordpress/api-fetch'; |
| 17 | import { select, useSelect } from '@wordpress/data'; |
| 18 | |
| 19 | /** |
| 20 | * Internal dependencies |
| 21 | */ |
| 22 | import { VkPanelIcon } from '@vkblocks/components/vk-icon'; |
| 23 | |
| 24 | const RELATED_POSTS_QUERY_TYPE = 'relatedPosts'; |
| 25 | |
| 26 | /** |
| 27 | * Structural Site Editor entities (templates, template parts, navigation) |
| 28 | * are not real content; core/editor's getCurrentPostType()/getCurrentPostId() |
| 29 | * report the entity itself (e.g. postId "themeSlug//templateSlug", not a |
| 30 | * number) while editing them, so callers must not treat those as a post. |
| 31 | * サイトエディタの構造的エンティティ(テンプレート等)は実コンテンツではない。 |
| 32 | * 編集中は core/editor の getCurrentPostType()/getCurrentPostId() がその |
| 33 | * エンティティ自身を返す(postId も数値ではなく "テーマ//テンプレート" 形式の |
| 34 | * 文字列になる)ため、投稿として扱ってはならない。 |
| 35 | */ |
| 36 | const NON_CONTENT_ENTITY_POST_TYPES = [ |
| 37 | 'wp_template', |
| 38 | 'wp_template_part', |
| 39 | 'wp_navigation', |
| 40 | ]; |
| 41 | |
| 42 | /** |
| 43 | * Attach the edited post ID to related-posts REST previews (request-time only). |
| 44 | * 関連記事 REST プレビューへ編集中投稿 ID をリクエスト時だけ付与する。 |
| 45 | */ |
| 46 | apiFetch.use((options, next) => { |
| 47 | if (typeof options?.path !== 'string') { |
| 48 | return next(options); |
| 49 | } |
| 50 | if ( |
| 51 | !options.path.includes( |
| 52 | `vkBlocksProQueryType=${RELATED_POSTS_QUERY_TYPE}` |
| 53 | ) || |
| 54 | options.path.includes('vkBlocksProRelatedPostId=') |
| 55 | ) { |
| 56 | return next(options); |
| 57 | } |
| 58 | |
| 59 | let postId; |
| 60 | try { |
| 61 | const editor = select('core/editor'); |
| 62 | // Editing a template/template-part directly (no post-in-progress): |
| 63 | // skip getCurrentPostId(), which would return the entity's own |
| 64 | // string ID and fail the REST integer param validation (400). |
| 65 | // テンプレート等を直接編集中は投稿が存在しないため getCurrentPostId() |
| 66 | // を使わない(エンティティ自身の文字列IDが返り、REST の integer 型 |
| 67 | // バリデーションで 400 になるため)。 |
| 68 | if ( |
| 69 | !NON_CONTENT_ENTITY_POST_TYPES.includes( |
| 70 | editor?.getCurrentPostType?.() |
| 71 | ) |
| 72 | ) { |
| 73 | postId = editor?.getCurrentPostId?.(); |
| 74 | } |
| 75 | } catch (e) { |
| 76 | // editor store may be unavailable outside the post editor. |
| 77 | } |
| 78 | if (!postId) { |
| 79 | try { |
| 80 | postId = select('core/edit-site')?.getEditedPostContext?.()?.postId; |
| 81 | } catch (e) { |
| 82 | // ignore |
| 83 | } |
| 84 | } |
| 85 | if (!postId) { |
| 86 | return next(options); |
| 87 | } |
| 88 | |
| 89 | const separator = options.path.includes('?') ? '&' : '?'; |
| 90 | return next({ |
| 91 | ...options, |
| 92 | path: `${options.path}${separator}vkBlocksProRelatedPostId=${postId}`, |
| 93 | }); |
| 94 | }); |
| 95 | |
| 96 | /** |
| 97 | * Whether related-posts mode is on (vkBlocksProQueryType). |
| 98 | * Matches PHP vk_blocks_is_related_posts_query_loop. |
| 99 | * 関連記事モード判定(PHP 側と同じ)。 |
| 100 | * |
| 101 | * @param {Object} blockAttributes Block attributes. |
| 102 | * @return {boolean} True when related-posts mode is active. |
| 103 | */ |
| 104 | const isRelatedPostsQueryAttributes = (blockAttributes) => |
| 105 | blockAttributes?.query?.vkBlocksProQueryType === RELATED_POSTS_QUERY_TYPE; |
| 106 | |
| 107 | /** |
| 108 | * Hidden variation for allowedControls only. Title stays "Query Loop" so the |
| 109 | * block is never renamed; no icon / description / attributes (avoids sticky ON). |
| 110 | * allowedControls 絞り込み専用。タイトルはクエリーループのまま。 |
| 111 | */ |
| 112 | registerBlockVariation('core/query', { |
| 113 | name: 'vk-blocks-related-posts', |
| 114 | // Intentionally no text domain: reuse WordPress core's "Query Loop" |
| 115 | // translation so this hidden variation title always matches the core block. |
| 116 | // テキストドメインは意図的に付けない。コアの「Query Loop」翻訳を流用し、 |
| 117 | // 隠しバリエーションのタイトルをコアのブロック名と常に一致させるため。 |
| 118 | title: __('Query Loop'), |
| 119 | // postCount + order (tiebreak). Other controls hide in related mode. |
| 120 | // 件数と並び順(tiebreak)のみ。関連� |
| 121 | �は現在の投稿に固定。 |
| 122 | allowedControls: ['postCount', 'order'], |
| 123 | scope: [], |
| 124 | isActive: (blockAttributes) => |
| 125 | isRelatedPostsQueryAttributes(blockAttributes), |
| 126 | }); |
| 127 | |
| 128 | /** |
| 129 | * VK Query Extension panel: related-posts toggle + modified-date order. |
| 130 | * 「VK クエリー拡張」: 関連記事トグルと更新日順。 |
| 131 | */ |
| 132 | const withQueryOrderExtension = createHigherOrderComponent( |
| 133 | (BlockEdit) => (props) => { |
| 134 | const { name, attributes, setAttributes } = props; |
| 135 | |
| 136 | if (name !== 'core/query') { |
| 137 | return <BlockEdit {...props} />; |
| 138 | } |
| 139 | |
| 140 | const { query } = attributes; |
| 141 | const { orderBy = 'date', order = 'desc' } = query || {}; |
| 142 | const isRelatedPostsQuery = isRelatedPostsQueryAttributes(attributes); |
| 143 | |
| 144 | // Disable (gray out) only when the edited entity is clearly not a post. |
| 145 | // Site editor / unknown context stays enabled; help text covers the caveat. |
| 146 | // 編集中が post 以外と分かるときだけ無効化(グレー)。テンプレ等は有効のまま。 |
| 147 | const editedPostType = useSelect((sel) => { |
| 148 | // Site Editor: templates preview a sample post via edited-post |
| 149 | // context. core/editor's getCurrentPostType() would return the |
| 150 | // template's own type (e.g. wp_template), not the previewed |
| 151 | // post's type, so prefer the preview context here. |
| 152 | // サイトエディタはテンプレート� |
| 153 | でサンプル投稿をプレビューする。 |
| 154 | // core/editor の getCurrentPostType() はテンプレート自身の型 |
| 155 | // (wp_template 等)を返してしまうため、プレビュー中の投稿の型を |
| 156 | // 優� |
| 157 | �して使う。 |
| 158 | try { |
| 159 | const templateContext = |
| 160 | sel('core/edit-site')?.getEditedPostContext?.(); |
| 161 | if (templateContext?.postType) { |
| 162 | return templateContext.postType; |
| 163 | } |
| 164 | } catch (e) { |
| 165 | // edit-site store may be unavailable outside the site editor. |
| 166 | } |
| 167 | try { |
| 168 | const currentPostType = |
| 169 | sel('core/editor')?.getCurrentPostType?.(); |
| 170 | // Structural entities (templates, template parts, nav) are not |
| 171 | // real content types; treat them as unknown rather than |
| 172 | // unsupported so template editing does not get blocked. |
| 173 | // テンプレート等の構造的エンティティは実コンテンツの投稿タイプ |
| 174 | // ではないため「不明」扱いにし、テンプレ編集を誤遮断しない。 |
| 175 | if ( |
| 176 | currentPostType && |
| 177 | !NON_CONTENT_ENTITY_POST_TYPES.includes(currentPostType) |
| 178 | ) { |
| 179 | return currentPostType; |
| 180 | } |
| 181 | } catch (e) { |
| 182 | // editor store may be unavailable outside the post editor. |
| 183 | } |
| 184 | return null; |
| 185 | }, []); |
| 186 | const isUnsupportedPostType = |
| 187 | typeof editedPostType === 'string' && editedPostType !== 'post'; |
| 188 | // Keep the control usable to turn OFF if it was already on. |
| 189 | // 既に ON のときは OFF に戻せるようにする。 |
| 190 | const isRelatedPostsToggleDisabled = |
| 191 | isUnsupportedPostType && !isRelatedPostsQuery; |
| 192 | |
| 193 | // Each sentence is wrapped in its own __() (one sentence per call). |
| 194 | // 翻訳関数は1文につき1つ(1関数=1文ルール)。 |
| 195 | // When disabled, drop the toggle help and explain via the warning Notice |
| 196 | // below so the reason reaches assistive tech without focusing the control. |
| 197 | // disabled 時はトグルの help を外し、下の warning Notice で理由を伝える |
| 198 | // (フォーカス非依存で AT に届くようにするため)。 |
| 199 | const relatedPostsHelp = isRelatedPostsToggleDisabled |
| 200 | ? undefined |
| 201 | : __( |
| 202 | 'Related posts are matched by the tags on the current post.', |
| 203 | 'vk-blocks' |
| 204 | ) + |
| 205 | ' ' + |
| 206 | __( |
| 207 | 'The list may be empty on pages or other post types, or until a new post is saved.', |
| 208 | 'vk-blocks' |
| 209 | ); |
| 210 | |
| 211 | /** |
| 212 | * Switch data source only (keep layout / perPage / order). |
| 213 | * ON: inherit=false (required for build_query_vars_from_query_block). |
| 214 | * OFF: clear vkBlocksProQueryType, then restore inherit. |
| 215 | * データソースだけ切替。ON 時 inherit=false。 |
| 216 | * OFF 時はフラグを消し、inherit を復� |
| 217 | �する。 |
| 218 | * |
| 219 | * vkBlocksProHadInherit / vkBlocksProInheritBackup are stored on the query |
| 220 | * attribute on purpose so OFF can restore the original inherit even after a |
| 221 | * reload; they persist in saved content but are harmless (unknown query keys |
| 222 | * are ignored on the front end). Moving them to useRef breaks that restore. |
| 223 | * これらの退避キーは意図的に query 属性へ保存する(リロード後も OFF で� |
| 224 | �の |
| 225 | * inherit を復� |
| 226 | �するため)。保存� |
| 227 | 容に残るが無害。useRef 化は復� |
| 228 | �を壊す。 |
| 229 | * |
| 230 | * @param {boolean} nextIsOn Whether related posts mode is being enabled. |
| 231 | */ |
| 232 | const toggleRelatedPosts = (nextIsOn) => { |
| 233 | const nextQuery = { ...(query || {}) }; |
| 234 | const nextAttributes = { query: nextQuery }; |
| 235 | const inheritBackupKey = 'vkBlocksProInheritBackup'; |
| 236 | const inheritHadKey = 'vkBlocksProHadInherit'; |
| 237 | |
| 238 | if (nextIsOn) { |
| 239 | nextQuery.vkBlocksProQueryType = RELATED_POSTS_QUERY_TYPE; |
| 240 | // Remember inherit only on first switch into related mode. |
| 241 | // 関連モードへ� |
| 242 | �る初回だけ inherit を退避する。 |
| 243 | if ( |
| 244 | !Object.prototype.hasOwnProperty.call( |
| 245 | nextQuery, |
| 246 | inheritHadKey |
| 247 | ) |
| 248 | ) { |
| 249 | const hadInherit = Object.prototype.hasOwnProperty.call( |
| 250 | nextQuery, |
| 251 | 'inherit' |
| 252 | ); |
| 253 | nextQuery[inheritHadKey] = hadInherit; |
| 254 | nextQuery[inheritBackupKey] = hadInherit |
| 255 | ? nextQuery.inherit |
| 256 | : undefined; |
| 257 | } |
| 258 | nextQuery.inherit = false; |
| 259 | } else { |
| 260 | delete nextQuery.vkBlocksProQueryType; |
| 261 | if ( |
| 262 | Object.prototype.hasOwnProperty.call( |
| 263 | nextQuery, |
| 264 | inheritHadKey |
| 265 | ) |
| 266 | ) { |
| 267 | const hadInherit = nextQuery[inheritHadKey]; |
| 268 | const backup = nextQuery[inheritBackupKey]; |
| 269 | delete nextQuery[inheritHadKey]; |
| 270 | delete nextQuery[inheritBackupKey]; |
| 271 | if (hadInherit) { |
| 272 | nextQuery.inherit = backup; |
| 273 | } else { |
| 274 | delete nextQuery.inherit; |
| 275 | } |
| 276 | } |
| 277 | } |
| 278 | |
| 279 | setAttributes(nextAttributes); |
| 280 | // ON: the Notice rendered while related mode is on announces itself on |
| 281 | // mount, so calling speak() here would double-announce to screen readers. |
| 282 | // OFF: no Notice is rendered, so announce the change here. |
| 283 | // ON時は Notice が mount 時に自動読み上げするため speak しない(重複回避)。 |
| 284 | // OFF時は Notice が無いのでここで通知する。 |
| 285 | if (!nextIsOn) { |
| 286 | speak(__('Related posts turned off.', 'vk-blocks'), 'polite'); |
| 287 | } |
| 288 | }; |
| 289 | |
| 290 | const getSelectValue = () => { |
| 291 | if (orderBy === 'modified') { |
| 292 | return order === 'desc' ? 'modified_desc' : 'modified_asc'; |
| 293 | } |
| 294 | return ''; |
| 295 | }; |
| 296 | |
| 297 | return ( |
| 298 | <> |
| 299 | <BlockEdit {...props} /> |
| 300 | <InspectorControls> |
| 301 | <PanelBody |
| 302 | title={__('VK Query Extension', 'vk-blocks')} |
| 303 | icon={ |
| 304 | <VkPanelIcon |
| 305 | isActive={ |
| 306 | isRelatedPostsQuery || |
| 307 | orderBy === 'modified' |
| 308 | } |
| 309 | /> |
| 310 | } |
| 311 | initialOpen={isRelatedPostsQuery} |
| 312 | > |
| 313 | <ToggleControl |
| 314 | label={__('Show related posts', 'vk-blocks')} |
| 315 | help={relatedPostsHelp} |
| 316 | checked={isRelatedPostsQuery} |
| 317 | disabled={isRelatedPostsToggleDisabled} |
| 318 | onChange={toggleRelatedPosts} |
| 319 | /> |
| 320 | {isRelatedPostsToggleDisabled && ( |
| 321 | <Notice status="warning" isDismissible={false}> |
| 322 | {__( |
| 323 | 'Related posts are unavailable for this content type.', |
| 324 | 'vk-blocks' |
| 325 | ) + |
| 326 | ' ' + |
| 327 | __( |
| 328 | 'Switch to editing a post that has tags to use this feature.', |
| 329 | 'vk-blocks' |
| 330 | )} |
| 331 | </Notice> |
| 332 | )} |
| 333 | {isRelatedPostsQuery && ( |
| 334 | <Notice status="info" isDismissible={false}> |
| 335 | {__( |
| 336 | 'Related posts are ranked by how many tags each post shares with the post you are editing, showing the most related first.', |
| 337 | 'vk-blocks' |
| 338 | ) + |
| 339 | ' ' + |
| 340 | __( |
| 341 | 'Order settings only affect posts that share the same number of tags.', |
| 342 | 'vk-blocks' |
| 343 | ) + |
| 344 | ' ' + |
| 345 | __( |
| 346 | 'Use the "Items per page" setting to change how many related posts appear.', |
| 347 | 'vk-blocks' |
| 348 | )} |
| 349 | </Notice> |
| 350 | )} |
| 351 | <SelectControl |
| 352 | label={__('Order by', 'vk-blocks')} |
| 353 | help={ |
| 354 | isRelatedPostsQuery |
| 355 | ? __( |
| 356 | 'Order for posts that share the same number of tags.', |
| 357 | 'vk-blocks' |
| 358 | ) |
| 359 | : __( |
| 360 | 'This setting will override the standard order setting.', |
| 361 | 'vk-blocks' |
| 362 | ) |
| 363 | } |
| 364 | value={getSelectValue()} |
| 365 | onChange={(value) => { |
| 366 | if (value === 'modified_desc') { |
| 367 | setAttributes({ |
| 368 | query: { |
| 369 | ...query, |
| 370 | orderBy: 'modified', |
| 371 | order: 'desc', |
| 372 | }, |
| 373 | }); |
| 374 | } else if (value === 'modified_asc') { |
| 375 | setAttributes({ |
| 376 | query: { |
| 377 | ...query, |
| 378 | orderBy: 'modified', |
| 379 | order: 'asc', |
| 380 | }, |
| 381 | }); |
| 382 | } else if (orderBy === 'modified') { |
| 383 | setAttributes({ |
| 384 | query: { |
| 385 | ...query, |
| 386 | orderBy: 'date', |
| 387 | order: 'desc', |
| 388 | }, |
| 389 | }); |
| 390 | } |
| 391 | }} |
| 392 | options={[ |
| 393 | { |
| 394 | value: '', |
| 395 | label: __( |
| 396 | 'Use standard options', |
| 397 | 'vk-blocks' |
| 398 | ), |
| 399 | }, |
| 400 | { |
| 401 | value: 'modified_desc', |
| 402 | label: __( |
| 403 | 'Modified (Newest to oldest)', |
| 404 | 'vk-blocks' |
| 405 | ), |
| 406 | }, |
| 407 | { |
| 408 | value: 'modified_asc', |
| 409 | label: __( |
| 410 | 'Modified (Oldest to newest)', |
| 411 | 'vk-blocks' |
| 412 | ), |
| 413 | }, |
| 414 | ]} |
| 415 | /> |
| 416 | </PanelBody> |
| 417 | </InspectorControls> |
| 418 | </> |
| 419 | ); |
| 420 | }, |
| 421 | 'withQueryOrderExtension' |
| 422 | ); |
| 423 | |
| 424 | addFilter( |
| 425 | 'editor.BlockEdit', |
| 426 | 'vk-blocks/query-order-extension', |
| 427 | withQueryOrderExtension |
| 428 | ); |
| 429 |