Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions assets/manual-posts.js
Original file line number Diff line number Diff line change
@@ -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
);
38 changes: 37 additions & 1 deletion hm-query-loop.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' ),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there an asset version we can use like those above? filemtime can give different results when the environment has multiple servers, probably fine for dev/local but could cause mystery issues/worse performance in production

[ 'in_footer' => false ]
);
}

/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'] ?? [];

Expand All @@ -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'];
}

Expand Down
Loading