From 41acf7d31acdbf20b7a6d0d966d61a6598513a8a Mon Sep 17 00:00:00 2001 From: Pamela Ribeiro <55361215+pamprn09@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:54:26 -0300 Subject: [PATCH 1/3] Add manual select --- assets/manual-posts.js | 140 +++++++++++++++++++++++++++++++++++++++++ hm-query-loop.php | 31 +++++++++ 2 files changed, 171 insertions(+) create mode 100644 assets/manual-posts.js diff --git a/assets/manual-posts.js b/assets/manual-posts.js new file mode 100644 index 0000000..8e3b20a --- /dev/null +++ b/assets/manual-posts.js @@ -0,0 +1,140 @@ +const { addFilter } = wp.hooks; +const { createHigherOrderComponent } = wp.compose; +const { InspectorControls } = wp.blockEditor; +const { PanelBody, FormTokenField } = wp.components; +const { Fragment, useState } = wp.element; +const apiFetch = wp.apiFetch; + + +/** + * Extend Query Loop attributes + */ +function addAttributes(settings, name) { + + if (name !== "core/query") { + return settings; + } + + settings.attributes = { + ...settings.attributes, + query: { + type: "object", + default: {} + } + }; + + return settings; +} + +addFilter( + "blocks.registerBlockType", + "manual-query-loop/add-attributes", + addAttributes +); + + +/** + * 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 tokens = selected.map((p) => p.title); + + const [ suggestions, setSuggestions ] = useState([]); + const [ map, setMap ] = useState({}); + + + const searchPosts = (value) => { + + if (!value || value.length < 2) return; + + apiFetch({ + path: "/wp/v2/search?search=" + encodeURIComponent(value) + "&type=post" + }).then((results) => { + + const titles = []; + const newMap = {}; + + results.forEach((r) => { + titles.push(r.title); + newMap[r.title] = r.id; + }); + + setSuggestions(titles); + setMap(newMap); + + }); + + }; + + + const onChange = (newTokens) => { + + const posts = newTokens.map((title) => { + + const existing = selected.find((p) => p.title === title); + if (existing) return existing; + + return { + id: map[title], + title: title + }; + + }).filter((p) => p && p.id); + + setAttributes({ + query: { + ...query, + manualPosts: posts, + perPage: posts.length, + per_page: posts.length, + offset: 0, + inherit: false + } + }); + + }; + + + return wp.element.createElement( + Fragment, + {}, + wp.element.createElement(BlockEdit, props), + wp.element.createElement( + InspectorControls, + {}, + wp.element.createElement( + PanelBody, + { title: "Manual Post Selection", initialOpen: true }, + 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..d341ea8 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 ] + ); } /** @@ -448,6 +456,29 @@ function exclude_posts_from_query( $query, $excluded_ids ) { function modify_query_from_block_attrs( $query = [], $attrs = [] ) { global $original_paged; + // Manual post selection: if manualPosts is set in the query context, + // override the query to show only those posts in the chosen order. + $manual_posts = $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'] ?? []; From 5280b9d612dbe4dd833bd2c7e4c0d9dcfb74762f Mon Sep 17 00:00:00 2001 From: Pamela Ribeiro <55361215+pamprn09@users.noreply.github.com> Date: Mon, 16 Mar 2026 08:07:39 -0300 Subject: [PATCH 2/3] Add manual post selection to Query Loop block --- assets/manual-posts.js | 73 ++++++++++++++++++------------------------ hm-query-loop.php | 7 +++- 2 files changed, 38 insertions(+), 42 deletions(-) diff --git a/assets/manual-posts.js b/assets/manual-posts.js index 8e3b20a..99c6863 100644 --- a/assets/manual-posts.js +++ b/assets/manual-posts.js @@ -5,32 +5,8 @@ const { PanelBody, FormTokenField } = wp.components; const { Fragment, useState } = wp.element; const apiFetch = wp.apiFetch; - -/** - * Extend Query Loop attributes - */ -function addAttributes(settings, name) { - - if (name !== "core/query") { - return settings; - } - - settings.attributes = { - ...settings.attributes, - query: { - type: "object", - default: {} - } - }; - - return settings; -} - -addFilter( - "blocks.registerBlockType", - "manual-query-loop/add-attributes", - addAttributes -); +// Module-level map avoids stale closure bugs with useState in onChange callbacks. +const _titleToId = {}; /** @@ -52,7 +28,6 @@ const withInspectorControls = createHigherOrderComponent((BlockEdit) => { const tokens = selected.map((p) => p.title); const [ suggestions, setSuggestions ] = useState([]); - const [ map, setMap ] = useState({}); const searchPosts = (value) => { @@ -64,15 +39,13 @@ const withInspectorControls = createHigherOrderComponent((BlockEdit) => { }).then((results) => { const titles = []; - const newMap = {}; results.forEach((r) => { titles.push(r.title); - newMap[r.title] = r.id; + _titleToId[r.title] = r.id; }); setSuggestions(titles); - setMap(newMap); }); @@ -87,22 +60,40 @@ const withInspectorControls = createHigherOrderComponent((BlockEdit) => { if (existing) return existing; return { - id: map[title], + id: _titleToId[title], title: title }; }).filter((p) => p && p.id); - setAttributes({ - query: { - ...query, - manualPosts: posts, - perPage: posts.length, - per_page: posts.length, - offset: 0, - inherit: false - } - }); + 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, + per_page: ids.length, + offset: 0, + inherit: false + } + }); + } else { + setAttributes({ + query: { + ...query, + manualPosts: [], + include: undefined, + perPage: query.perPage, + per_page: query.per_page, + offset: query.offset + } + }); + } }; diff --git a/hm-query-loop.php b/hm-query-loop.php index d341ea8..61e6e64 100644 --- a/hm-query-loop.php +++ b/hm-query-loop.php @@ -380,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 @@ -493,7 +497,8 @@ function( $p ) { } // 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']; } From ed972451b53c52ca34c2dcfedd597b59e227a6c9 Mon Sep 17 00:00:00 2001 From: Pamela Ribeiro <55361215+pamprn09@users.noreply.github.com> Date: Mon, 16 Mar 2026 08:40:58 -0300 Subject: [PATCH 3/3] Add toggle for manual posts, saving it's state --- assets/manual-posts.js | 64 ++++++++++++++++++++++++++++++++++-------- hm-query-loop.php | 6 ++-- 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/assets/manual-posts.js b/assets/manual-posts.js index 99c6863..663c89d 100644 --- a/assets/manual-posts.js +++ b/assets/manual-posts.js @@ -1,7 +1,7 @@ const { addFilter } = wp.hooks; const { createHigherOrderComponent } = wp.compose; const { InspectorControls } = wp.blockEditor; -const { PanelBody, FormTokenField } = wp.components; +const { PanelBody, FormTokenField, ToggleControl } = wp.components; const { Fragment, useState } = wp.element; const apiFetch = wp.apiFetch; @@ -24,12 +24,52 @@ const withInspectorControls = createHigherOrderComponent((BlockEdit) => { 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; @@ -77,20 +117,15 @@ const withInspectorControls = createHigherOrderComponent((BlockEdit) => { manualPosts: posts, include: ids, perPage: ids.length, - per_page: ids.length, - offset: 0, - inherit: false } }); } else { + // eslint-disable-next-line no-unused-vars + const { include: _inc, perPage: _pp, per_page: _ppp, ...restQuery } = query; setAttributes({ query: { - ...query, - manualPosts: [], - include: undefined, - perPage: query.perPage, - per_page: query.per_page, - offset: query.offset + ...restQuery, + manualPosts: [] } }); } @@ -107,8 +142,13 @@ const withInspectorControls = createHigherOrderComponent((BlockEdit) => { {}, wp.element.createElement( PanelBody, - { title: "Manual Post Selection", initialOpen: true }, - wp.element.createElement(FormTokenField, { + { 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, diff --git a/hm-query-loop.php b/hm-query-loop.php index 61e6e64..7579817 100644 --- a/hm-query-loop.php +++ b/hm-query-loop.php @@ -460,9 +460,9 @@ function exclude_posts_from_query( $query, $excluded_ids ) { function modify_query_from_block_attrs( $query = [], $attrs = [] ) { global $original_paged; - // Manual post selection: if manualPosts is set in the query context, - // override the query to show only those posts in the chosen order. - $manual_posts = $attrs['query']['manualPosts'] ?? []; + // 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(