diff --git a/assets/manual-posts.js b/assets/manual-posts.js new file mode 100644 index 0000000..663c89d --- /dev/null +++ b/assets/manual-posts.js @@ -0,0 +1,171 @@ +const { addFilter } = wp.hooks; +const { createHigherOrderComponent } = wp.compose; +const { InspectorControls } = wp.blockEditor; +const { PanelBody, FormTokenField, ToggleControl } = wp.components; +const { Fragment, useState } = wp.element; +const apiFetch = wp.apiFetch; + +// Module-level map avoids stale closure bugs with useState in onChange callbacks. +const _titleToId = {}; + + +/** + * Inspector UI + */ +const withInspectorControls = createHigherOrderComponent((BlockEdit) => { + + return (props) => { + + if (props.name !== "core/query") { + return wp.element.createElement(BlockEdit, props); + } + + const { attributes, setAttributes } = props; + + const query = attributes.query || {}; + const selected = query.manualPosts || []; + const isManual = query.manualSelect === true; + + const tokens = selected.map((p) => p.title); + + const [ suggestions, setSuggestions ] = useState([]); + + + const onToggleManual = (value) => { + if ( value ) { + // Re-enable: manualPosts already stored in query (preserved from last session). + // Derive include + perPage from them so the editor preview updates immediately. + const restored = query.manualPosts || []; + const ids = restored.map( (p) => p.id ); + + // Rebuild _titleToId so onChange works with existing selections immediately. + restored.forEach( (p) => { _titleToId[ p.title ] = p.id; } ); + + setAttributes({ + query: { + ...query, + manualSelect: true, + _previousInherit: query.inherit, + _previousPerPage: query.perPage, + inherit: false, + ...( ids.length > 0 && { + include: ids, + perPage: ids.length, + } ) + } + }); + } else { + // Disable: remove only the active-mode derived keys (include, perPage). + // manualPosts is intentionally kept so it can be restored on re-enable. + // eslint-disable-next-line no-unused-vars + const { include: _inc, manualSelect: _ms, _previousInherit, perPage: _pp, per_page: _ppp, _previousPerPage, ...restQuery } = query; + setAttributes({ + query: { + ...restQuery, + inherit: _previousInherit !== undefined ? _previousInherit : restQuery.inherit || false, + ...( _previousPerPage !== undefined && { perPage: _previousPerPage } ) + } + }); + } + }; + + + const searchPosts = (value) => { + + if (!value || value.length < 2) return; + + apiFetch({ + path: "/wp/v2/search?search=" + encodeURIComponent(value) + "&type=post" + }).then((results) => { + + const titles = []; + + results.forEach((r) => { + titles.push(r.title); + _titleToId[r.title] = r.id; + }); + + setSuggestions(titles); + + }); + + }; + + + const onChange = (newTokens) => { + + const posts = newTokens.map((title) => { + + const existing = selected.find((p) => p.title === title); + if (existing) return existing; + + return { + id: _titleToId[title], + title: title + }; + + }).filter((p) => p && p.id); + + const ids = posts.map((p) => p.id); + + // When posts are selected: force include + disable inherit so the + // editor preview updates live. When cleared: restore normal behavior. + if ( ids.length > 0 ) { + setAttributes({ + query: { + ...query, + manualPosts: posts, + include: ids, + perPage: ids.length, + } + }); + } else { + // eslint-disable-next-line no-unused-vars + const { include: _inc, perPage: _pp, per_page: _ppp, ...restQuery } = query; + setAttributes({ + query: { + ...restQuery, + manualPosts: [] + } + }); + } + + }; + + + return wp.element.createElement( + Fragment, + {}, + wp.element.createElement(BlockEdit, props), + wp.element.createElement( + InspectorControls, + {}, + wp.element.createElement( + PanelBody, + { title: "Manual Post Selection", initialOpen: false }, + wp.element.createElement(ToggleControl, { + label: "Enable manual post selection", + checked: isManual, + onChange: onToggleManual + }), + isManual && wp.element.createElement(FormTokenField, { + label: "Search and select posts", + value: tokens, + suggestions: suggestions, + onInputChange: searchPosts, + onChange: onChange + }) + ) + ) + ); + + }; + +}, "withInspectorControls"); + + +addFilter( + "editor.BlockEdit", + "manual-query-loop/add-inspector", + withInspectorControls +); diff --git a/hm-query-loop.php b/hm-query-loop.php index 372b881..7579817 100644 --- a/hm-query-loop.php +++ b/hm-query-loop.php @@ -81,6 +81,14 @@ function enqueue_block_editor_assets() { [], $asset_file['version'] ); + + wp_enqueue_script( + 'hm-query-loop-manual-posts', + HM_QUERY_LOOP_URL . 'assets/manual-posts.js', + [ 'wp-blocks', 'wp-element', 'wp-hooks', 'wp-components', 'wp-compose', 'wp-block-editor', 'wp-api-fetch' ], + filemtime( HM_QUERY_LOOP_PATH . 'assets/manual-posts.js' ), + [ 'in_footer' => false ] + ); } /** @@ -372,6 +380,10 @@ function filter_query_loop_block_query_vars( $query, WP_Block $block ) { $block->parsed_block['attrs']['hmQueryLoop'] ?? [], $block->context['hmQueryLoop'] ?? [], ); + // Pass the parent core/query block's query object (including manualPosts) to the modifier. + if ( empty( $attrs['query'] ) ) { + $attrs['query'] = $block->context['query'] ?? []; + } $query_id = $block->context['queryId'] ?? 0; // Initialize tracking array for this query loop if not exists @@ -448,6 +460,29 @@ function exclude_posts_from_query( $query, $excluded_ids ) { function modify_query_from_block_attrs( $query = [], $attrs = [] ) { global $original_paged; + // Manual post selection: only active when manualSelect flag is set. + $manual_select = $attrs['query']['manualSelect'] ?? false; + $manual_posts = $manual_select ? ( $attrs['query']['manualPosts'] ?? [] ) : []; + if ( ! empty( $manual_posts ) ) { + $ids = array_values( + array_filter( + array_map( + function( $p ) { + return is_array( $p ) ? intval( $p['id'] ) : intval( $p ); + }, + $manual_posts + ) + ) + ); + + if ( ! empty( $ids ) ) { + $query['post__in'] = $ids; + $query['orderby'] = 'post__in'; + $query['posts_per_page'] = count( $ids ); + $query['ignore_sticky_posts'] = true; + } + } + // Get the hmQueryLoop settings object. $settings = $attrs['hmQueryLoop'] ?? []; @@ -462,7 +497,8 @@ function modify_query_from_block_attrs( $query = [], $attrs = [] ) { } // Apply custom posts per page if set and is a valid number. - if ( isset( $settings['perPage'] ) && is_numeric( $settings['perPage'] ) && $settings['perPage'] > 0 ) { + // Skip when manual post selection is active — it already sets posts_per_page precisely. + if ( empty( $manual_posts ) && isset( $settings['perPage'] ) && is_numeric( $settings['perPage'] ) && $settings['perPage'] > 0 ) { $query['posts_per_page'] = (int) $settings['perPage']; }