diff --git a/build/index.asset.php b/build/index.asset.php index 77c6b8e..4717e8a 100644 --- a/build/index.asset.php +++ b/build/index.asset.php @@ -1 +1 @@ - array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n'), 'version' => '9416f37d7ab747ea1de8'); + array('react', 'wp-api-fetch', 'wp-blocks', 'wp-compose', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n'), 'version' => '142c185eb9e8b0c1f123'); diff --git a/build/index.js b/build/index.js index 9f02dd9..4d54e48 100644 --- a/build/index.js +++ b/build/index.js @@ -1 +1 @@ -(()=>{"use strict";var e={n:t=>{var o=t&&t.__esModule?()=>t.default:()=>t;return e.d(o,{a:o}),o},d:(t,o)=>{for(var n in o)e.o(o,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:o[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=window.wp.blocks,o=window.wp.i18n,n="next-posts-block/query";(0,t.registerBlockVariation)("core/query",{name:"next-posts",title:(0,o.__)("Next Posts","next-posts-block"),description:(0,o.__)("Displays the N posts that sequentially follow (or precede) the current post, with wrap-around.","next-posts-block"),icon:"list-view",category:"theme",keywords:[(0,o.__)("next","next-posts-block"),(0,o.__)("previous","next-posts-block"),(0,o.__)("sequence","next-posts-block"),(0,o.__)("related","next-posts-block")],scope:["inserter"],attributes:{namespace:n,query:{postType:"post",perPage:3,inherit:!1,offset:0,orderBy:"date",order:"asc",excludeSticky:!1}},innerBlocks:[["core/post-template",{},[["core/post-featured-image"],["core/post-title",{level:3,isLink:!0}],["core/post-excerpt"],["core/read-more"]]]],isActive:({namespace:e})=>e===n});const r=window.React,s=window.wp.hooks,c=window.wp.compose,i=window.wp.blockEditor,l=window.wp.components,u=window.wp.element,a=(0,c.createHigherOrderComponent)(e=>t=>{if("core/query"!==t.name||"next-posts-block/query"!==t.attributes?.namespace)return(0,r.createElement)(e,{...t});const n=Boolean(t.attributes?.query?.excludeSticky);return(0,r.createElement)(u.Fragment,null,(0,r.createElement)(e,{...t}),(0,r.createElement)(i.InspectorControls,null,(0,r.createElement)(l.PanelBody,{title:(0,o.__)("Sequential settings","next-posts-block"),initialOpen:!0},(0,r.createElement)(l.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,o.__)("Exclude sticky posts from the sequence","next-posts-block"),help:(0,o.__)("Sticky posts are removed from the canonical list used to build the sequence.","next-posts-block"),checked:n,onChange:e=>{t.setAttributes({query:{...t.attributes.query,excludeSticky:e}})}}))))},"withSequentialSettings");(0,s.addFilter)("editor.BlockEdit","next-posts-block/sequential-settings",a);const d=window.wp.apiFetch;var p=e.n(d);const m=window.wp.data,y=/\/wp\/v2\/([a-z0-9_-]+)(\?|$)/i;p().use((e,t)=>{const o=e.url||e.path||"";if(!o.match(y))return t(e);const n=function e(t){for(const r of t){var o,n;if("core/query"===r.name&&"next-posts-block/query"===r.attributes?.namespace)return{orderby:null!==(o=r.attributes.query?.orderBy)&&void 0!==o?o:"date",order:null!==(n=r.attributes.query?.order)&&void 0!==n?n:"asc",excludeSticky:Boolean(r.attributes.query?.excludeSticky)};if(r.innerBlocks?.length){const t=e(r.innerBlocks);if(t)return t}}return null}(null!==(r=(0,m.select)("core/block-editor")?.getBlocks())&&void 0!==r?r:[]);var r;if(!n)return t(e);if(o.includes("sequential_block="))return t(e);const s=(0,m.select)("core/editor")?.getCurrentPostId(),c=["sequential_block=1",`sequential_orderby=${encodeURIComponent(n.orderby)}`,`sequential_order=${encodeURIComponent(n.order)}`];n.excludeSticky&&c.push("sequential_exclude_sticky=1"),s&&c.push(`sequential_context_post=${s}`);const i=o.includes("?")?"&":"?",l=`${o}${i}${c.join("&")}`;return t({...e,url:e.url?l:void 0,path:e.path?l:void 0})});const b="next-posts-block-hide-query-type",k=[/sticky\s+posts?/i,/publications?\s+épinglées?/i,/publicaciones?\s+fijadas?/i],f="data-sequential-hidden";function w(e){const t=e.querySelector("label, legend, .components-base-control__label");if(!t)return!1;const o=t.textContent.trim();return k.some(e=>e.test(o))}function q(){const e=document.querySelectorAll(".editor-block-inspector .components-tools-panel-item, .block-editor-block-inspector .components-tools-panel-item");for(const t of e)t.hasAttribute(f)||w(t)&&(t.setAttribute(f,"1"),t.style.display="none")}const h=(0,c.createHigherOrderComponent)(e=>t=>{const o="core/query"===t.name&&"next-posts-block/query"===t.attributes?.namespace,n=!0===t.attributes?.query?.inherit;return(0,u.useEffect)(()=>{o&&n&&t.setAttributes({query:{...t.attributes.query,inherit:!1}})},[o,n]),(0,u.useEffect)(()=>{if(!o||!t.isSelected)return;!function(){if(document.getElementById(b))return;const e=document.createElement("style");e.id=b,e.textContent=".components-tools-panel-item:has(.components-toggle-group-control){display:none!important}",document.head.appendChild(e)}();const e=function(){q();const e=document.querySelector(".interface-interface-skeleton__sidebar")||document.body,t=new MutationObserver(e=>{e.some(e=>e.addedNodes.length>0||e.removedNodes.length>0)&&q()});return t.observe(e,{childList:!0,subtree:!0}),()=>{t.disconnect(),function(){const e=document.querySelectorAll(`[${f}]`);for(const t of e)t.style.display="",t.removeAttribute(f)}()}}();return()=>{!function(){const e=document.getElementById(b);e&&e.remove()}(),e()}},[o,t.isSelected]),(0,r.createElement)(e,{...t})},"withLockedCustomQuery");(0,s.addFilter)("editor.BlockEdit","next-posts-block/lock-custom-query",h)})(); \ No newline at end of file +(()=>{"use strict";var e={n:t=>{var o=t&&t.__esModule?()=>t.default:()=>t;return e.d(o,{a:o}),o},d:(t,o)=>{for(var n in o)e.o(o,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:o[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=window.wp.blocks,o=window.wp.i18n,n="next-posts-block/query";(0,t.registerBlockVariation)("core/query",{name:"next-posts",title:(0,o.__)("Next Posts","next-posts-block"),description:(0,o.__)("Displays the N posts that sequentially follow (or precede) the current post, with wrap-around.","next-posts-block"),icon:"list-view",category:"theme",keywords:[(0,o.__)("next","next-posts-block"),(0,o.__)("previous","next-posts-block"),(0,o.__)("sequence","next-posts-block"),(0,o.__)("related","next-posts-block")],scope:["inserter"],attributes:{namespace:n,query:{postType:"post",perPage:3,inherit:!1,offset:0,orderBy:"date",order:"asc"}},innerBlocks:[["core/post-template",{},[["core/post-featured-image"],["core/post-title",{level:3,isLink:!0}],["core/post-excerpt"],["core/read-more"]]]],isActive:({namespace:e})=>e===n});const r=window.wp.apiFetch;var s=e.n(r);const c=window.wp.data,i=/\/wp\/v2\/([a-z0-9_-]+)(\?|$)/i;s().use((e,t)=>{const o=e.url||e.path||"";if(!o.match(i))return t(e);const n=function e(t){for(const n of t){var o;if("core/query"===n.name&&"next-posts-block/query"===n.attributes?.namespace)return null!==(o=n.attributes.query)&&void 0!==o?o:{};if(n.innerBlocks?.length){const t=e(n.innerBlocks);if(t)return t}}return null}(null!==(r=(0,c.select)("core/block-editor")?.getBlocks())&&void 0!==r?r:[]);var r;if(!n)return t(e);if(o.includes("sequential_block="))return t(e);const s=(0,c.select)("core/editor")?.getCurrentPostId(),l=["sequential_block=1",`sequential_query_attrs=${encodeURIComponent(JSON.stringify(n))}`];s&&l.push(`sequential_context_post=${s}`);const u=o.includes("?")?"&":"?",a=`${o}${u}${l.join("&")}`;return t({...e,url:e.url?a:void 0,path:e.path?a:void 0})});const l=window.React,u=window.wp.hooks,a=window.wp.compose,p=window.wp.element,d="next-posts-block-hide-query-type",m=(0,a.createHigherOrderComponent)(e=>t=>{const o="core/query"===t.name&&"next-posts-block/query"===t.attributes?.namespace,n=!0===t.attributes?.query?.inherit;return(0,p.useEffect)(()=>{o&&n&&t.setAttributes({query:{...t.attributes.query,inherit:!1}})},[o,n]),(0,p.useEffect)(()=>{if(o&&t.isSelected)return function(){if(document.getElementById(d))return;const e=document.createElement("style");e.id=d,e.textContent=".components-tools-panel-item:has(.components-toggle-group-control){display:none!important}",document.head.appendChild(e)}(),()=>{!function(){const e=document.getElementById(d);e&&e.remove()}()}},[o,t.isSelected]),(0,l.createElement)(e,{...t})},"withLockedCustomQuery");(0,u.addFilter)("editor.BlockEdit","next-posts-block/lock-custom-query",m)})(); \ No newline at end of file diff --git a/includes/CanonicalList.php b/includes/CanonicalList.php index bce0917..2da591e 100644 --- a/includes/CanonicalList.php +++ b/includes/CanonicalList.php @@ -17,59 +17,171 @@ final class CanonicalList private const ALLOWED_ORDERBY = ['date', 'title']; private const ALLOWED_ORDER = ['ASC', 'DESC']; + private const ALLOWED_STICKY = ['', 'ignore', 'exclude', 'only']; /** - * @param string $post_type WordPress post type slug. - * @param string $orderby 'date' or 'title'. - * @param string $order 'ASC' or 'DESC'. - * @param bool $exclude_sticky When true, sticky posts are removed from the list. + * @param array $query_attrs Block `query` attributes bag. * @return int[] Ordered list of published post IDs. */ - public static function get( - string $post_type, - string $orderby = 'date', - string $order = 'ASC', - bool $exclude_sticky = false - ): array { - if (!post_type_exists($post_type)) { + public static function build(array $query_attrs): array + { + $normalized = self::normalize($query_attrs); + if ($normalized === null) { return []; } - $orderby = in_array($orderby, self::ALLOWED_ORDERBY, true) ? $orderby : 'date'; - $order = in_array(strtoupper($order), self::ALLOWED_ORDER, true) ? strtoupper($order) : 'ASC'; - $last_changed = wp_cache_get_last_changed('posts'); - $cache_flag = $exclude_sticky ? 'no_sticky' : 'with_sticky'; - $cache_key = "canonical:{$post_type}:{$orderby}:{$order}:{$cache_flag}:{$last_changed}"; + $cache_key = 'canonical:' . md5(serialize($normalized)) . ':' . $last_changed; $cached = wp_cache_get($cache_key, self::CACHE_GROUP); if ($cached !== false) { return $cached; } + $args = self::build_query_args($normalized); + $query = new \WP_Query($args); + $ids = array_map('intval', $query->posts); + + $ids = self::apply_sticky_mode($ids, $normalized['sticky'], $args); + + wp_cache_set($cache_key, $ids, self::CACHE_GROUP, self::CACHE_TTL); + return $ids; + } + + /** + * @param array $query_attrs + * @return array|null Null if postType invalid. + */ + private static function normalize(array $query_attrs): ?array + { + $post_type = (string) ($query_attrs['postType'] ?? 'post'); + if (!post_type_exists($post_type)) { + return null; + } + + $orderby_raw = (string) ($query_attrs['orderBy'] ?? 'date'); + $order_raw = strtoupper((string) ($query_attrs['order'] ?? 'ASC')); + $sticky_raw = (string) ($query_attrs['sticky'] ?? ''); + + $search = trim((string) ($query_attrs['search'] ?? '')); + + $author_raw = (string) ($query_attrs['author'] ?? ''); + $author = array_values(array_filter( + array_map('intval', $author_raw === '' ? [] : explode(',', $author_raw)), + static fn($id) => $id > 0 + )); + + $tax_query_raw = $query_attrs['taxQuery'] ?? []; + $tax_query = []; + if (is_array($tax_query_raw)) { + foreach ($tax_query_raw as $taxonomy => $term_ids) { + if (!taxonomy_exists((string) $taxonomy)) { + continue; + } + $ids = array_values(array_filter( + array_map('intval', (array) $term_ids), + static fn($id) => $id > 0 + )); + if (!empty($ids)) { + $tax_query[(string) $taxonomy] = $ids; + } + } + } + + return [ + 'postType' => $post_type, + 'orderBy' => in_array($orderby_raw, self::ALLOWED_ORDERBY, true) ? $orderby_raw : 'date', + 'order' => in_array($order_raw, self::ALLOWED_ORDER, true) ? $order_raw : 'ASC', + 'sticky' => in_array($sticky_raw, self::ALLOWED_STICKY, true) ? $sticky_raw : '', + 'taxQuery' => $tax_query, + 'author' => $author, + 'search' => $search, + ]; + } + + /** + * @param array $n Normalized attrs. + * @return array WP_Query args. + */ + private static function build_query_args(array $n): array + { $args = [ - 'post_type' => $post_type, - 'post_status' => 'publish', - 'posts_per_page' => -1, - 'fields' => 'ids', - 'orderby' => $orderby, - 'order' => $order, - 'no_found_rows' => true, - 'suppress_filters' => true, + 'post_type' => $n['postType'], + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'fields' => 'ids', + 'orderby' => $n['orderBy'], + 'order' => $n['order'], + 'no_found_rows' => true, + 'suppress_filters' => true, 'ignore_sticky_posts' => 1, ]; - if ($exclude_sticky) { - $sticky_ids = get_option('sticky_posts', []); - if (!empty($sticky_ids)) { - $args['post__not_in'] = array_map('intval', (array) $sticky_ids); + if (!empty($n['author'])) { + $args['author__in'] = $n['author']; + } + + if ($n['search'] !== '') { + $args['s'] = $n['search']; + } + + if (!empty($n['taxQuery'])) { + $clauses = []; + foreach ($n['taxQuery'] as $taxonomy => $term_ids) { + $clauses[] = [ + 'taxonomy' => $taxonomy, + 'field' => 'term_id', + 'terms' => $term_ids, + 'operator' => 'IN', + ]; + } + if (count($clauses) > 1) { + $clauses['relation'] = 'AND'; } + $args['tax_query'] = $clauses; } - $query = new \WP_Query($args); + return $args; + } + + /** + * @param int[] $ids + * @param string $mode + * @param array $args + * @return int[] + */ + private static function apply_sticky_mode(array $ids, string $mode, array $args): array + { + if ($mode === 'ignore' || empty($ids)) { + return $ids; + } + + $sticky_option = array_map('intval', (array) get_option('sticky_posts', [])); + + if ($mode === '') { + if (empty($sticky_option)) { + return $ids; + } + $matching_stickies = array_values(array_intersect($ids, $sticky_option)); + $non_stickies = array_values(array_diff($ids, $matching_stickies)); + return array_merge($matching_stickies, $non_stickies); + } + + if ($mode === 'exclude') { + if (empty($sticky_option)) { + return $ids; + } + return array_values(array_diff($ids, $sticky_option)); + } + + if ($mode === 'only') { + if (empty($sticky_option)) { + return []; + } + return array_values(array_intersect($ids, $sticky_option)); + } - $ids = array_map('intval', $query->posts); - wp_cache_set($cache_key, $ids, self::CACHE_GROUP, self::CACHE_TTL); return $ids; } + } diff --git a/includes/QueryFilter.php b/includes/QueryFilter.php index 8f2ab0f..b943681 100644 --- a/includes/QueryFilter.php +++ b/includes/QueryFilter.php @@ -38,14 +38,8 @@ final class QueryFilter /** @var bool Whether the currently-rendering core/query is our variation. */ private static bool $is_sequential = false; - /** @var string Native orderBy from the block ('date' or 'title'). */ - private static string $orderby = 'date'; - - /** @var string Native order from the block ('asc' or 'desc'). */ - private static string $order = 'asc'; - - /** @var bool Custom excludeSticky attribute from the block. */ - private static bool $exclude_sticky = false; + /** @var array Captured native query attrs from pre_render. */ + private static array $query_attrs = []; /** * Hook: pre_render_block (priority 10, 2 args) @@ -68,16 +62,14 @@ public function pre_render($pre_render, array $parsed_block) // not wipe the state we just armed, as they fire pre_render_block between // our core/query's pre_render and query_loop_block_query_vars consumption. self::$is_sequential = false; - self::$exclude_sticky = false; + self::$query_attrs = []; if (($parsed_block['attrs']['namespace'] ?? '') !== self::NAMESPACE) { return $pre_render; } self::$is_sequential = true; - self::$orderby = (string) ($parsed_block['attrs']['query']['orderBy'] ?? 'date'); - self::$order = (string) ($parsed_block['attrs']['query']['order'] ?? 'asc'); - self::$exclude_sticky = (bool) ($parsed_block['attrs']['query']['excludeSticky'] ?? false); + self::$query_attrs = (array) ($parsed_block['attrs']['query'] ?? []); return $pre_render; } @@ -95,16 +87,15 @@ public function filter_query_vars(array $query, WP_Block $block, int $page): arr } self::$is_sequential = false; - $exclude_sticky = self::$exclude_sticky; - self::$exclude_sticky = false; + $attrs = self::$query_attrs; + self::$query_attrs = []; - $post_type = (string) ($block->context['query']['postType'] ?? 'post'); $raw_count = (int) ($block->context['query']['perPage'] ?? 3); $count = max(self::MIN_COUNT, min(self::MAX_COUNT, $raw_count)); - // Build canonical list with the user's chosen sort order. - // Resolver always goes forward — the list order defines "next". - $all_ids = CanonicalList::get($post_type, self::$orderby, self::$order, $exclude_sticky); + // Build canonical list from the full native attrs bag; resolver + // always walks forward, so the list order defines "next". + $all_ids = CanonicalList::build($attrs); $current_id = (new ContextDetector())->current_post_id(); $resolved = $current_id === null @@ -146,13 +137,24 @@ public function filter_rest_query(string $post_type, array $args, WP_REST_Reques return $args; } - $orderby = (string) ($request->get_param('sequential_orderby') ?? 'date'); - $order = (string) ($request->get_param('sequential_order') ?? 'asc'); - $exclude_sticky = (bool) $request->get_param('sequential_exclude_sticky'); + $raw_attrs = $request->get_param('sequential_query_attrs'); + $attrs = []; + if (is_string($raw_attrs) && $raw_attrs !== '') { + $decoded = json_decode($raw_attrs, true); + if (is_array($decoded)) { + $attrs = $decoded; + } + } + + // REST route is post-type scoped; respect that if attrs omit postType. + if (empty($attrs['postType'])) { + $attrs['postType'] = $post_type; + } + $raw_count = (int) ($args['posts_per_page'] ?? 3); $count = max(self::MIN_COUNT, min(self::MAX_COUNT, $raw_count)); - $all_ids = CanonicalList::get($post_type, $orderby, $order, $exclude_sticky); + $all_ids = CanonicalList::build($attrs); $resolved = $context_post ? (new SequentialResolver())->resolve($all_ids, $context_post, 'asc', $count) : array_slice($all_ids, 0, $count); diff --git a/languages/next-posts-block-fr_FR-79a52190a960c6e16ae31bdaa2d99ecd.json b/languages/next-posts-block-fr_FR-79a52190a960c6e16ae31bdaa2d99ecd.json new file mode 100644 index 0000000..bb1b286 --- /dev/null +++ b/languages/next-posts-block-fr_FR-79a52190a960c6e16ae31bdaa2d99ecd.json @@ -0,0 +1 @@ +{"translation-revision-date":"2026-04-17 14:30+0200","generator":"WP-CLI\/2.13.0-alpha-275d793","source":"src\/variation.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"fr_FR","plural-forms":"nplurals=2; plural=(n > 1);"},"Next Posts":["Publications suivantes"],"Displays the N posts that sequentially follow (or precede) the current post, with wrap-around.":["Affiche les N publications qui suivent (ou pr\u00e9c\u00e8dent) s\u00e9quentiellement la publication actuelle, avec retour au d\u00e9but."],"next":["suivant"],"previous":["pr\u00e9c\u00e9dent"],"sequence":["s\u00e9quence"],"related":["connexe"]}}} \ No newline at end of file diff --git a/languages/next-posts-block-fr_FR-dfbff627e6c248bcb3b61d7d06da9ca9.json b/languages/next-posts-block-fr_FR-dfbff627e6c248bcb3b61d7d06da9ca9.json index b1fad6c..6320f95 100644 --- a/languages/next-posts-block-fr_FR-dfbff627e6c248bcb3b61d7d06da9ca9.json +++ b/languages/next-posts-block-fr_FR-dfbff627e6c248bcb3b61d7d06da9ca9.json @@ -1 +1 @@ -{"translation-revision-date":"2026-04-17 14:30+0200","generator":"WP-CLI\/2.13.0-alpha-275d793","source":"build\/index.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"fr_FR","plural-forms":"nplurals=2; plural=(n > 1);"},"Next Posts":["Publications suivantes"],"Displays the N posts that sequentially follow (or precede) the current post, with wrap-around.":["Affiche les N publications qui suivent (ou pr\u00e9c\u00e8dent) s\u00e9quentiellement la publication actuelle, avec retour au d\u00e9but."],"next":["suivant"],"previous":["pr\u00e9c\u00e9dent"],"sequence":["s\u00e9quence"],"related":["connexe"],"Sequential settings":["R\u00e9glages de s\u00e9quence"],"Exclude sticky posts from the sequence":["Exclure les publications \u00e9pingl\u00e9es de la s\u00e9quence"],"Sticky posts are removed from the canonical list used to build the sequence.":["Les publications \u00e9pingl\u00e9es sont retir\u00e9es de la liste canonique utilis\u00e9e pour construire la s\u00e9quence."]}}} \ No newline at end of file +{"translation-revision-date":"2026-04-17 14:30+0200","generator":"WP-CLI\/2.13.0-alpha-275d793","source":"build\/index.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"fr_FR","plural-forms":"nplurals=2; plural=(n > 1);"},"Next Posts":["Publications suivantes"],"Displays the N posts that sequentially follow (or precede) the current post, with wrap-around.":["Affiche les N publications qui suivent (ou pr\u00e9c\u00e8dent) s\u00e9quentiellement la publication actuelle, avec retour au d\u00e9but."],"next":["suivant"],"previous":["pr\u00e9c\u00e9dent"],"sequence":["s\u00e9quence"],"related":["connexe"]}}} \ No newline at end of file diff --git a/languages/next-posts-block-fr_FR.mo b/languages/next-posts-block-fr_FR.mo index 96c9448..6907ee3 100644 Binary files a/languages/next-posts-block-fr_FR.mo and b/languages/next-posts-block-fr_FR.mo differ diff --git a/languages/next-posts-block-fr_FR.po b/languages/next-posts-block-fr_FR.po index 037612e..48a998c 100644 --- a/languages/next-posts-block-fr_FR.po +++ b/languages/next-posts-block-fr_FR.po @@ -63,18 +63,3 @@ msgstr "séquence" #: build/index.js:1 src/variation.js:27 msgid "related" msgstr "connexe" - -#: build/index.js:1 src/inspector-controls.js:48 -msgid "Sequential settings" -msgstr "Réglages de séquence" - -#: build/index.js:1 src/inspector-controls.js:56 -msgid "Exclude sticky posts from the sequence" -msgstr "Exclure les publications épinglées de la séquence" - -#: build/index.js:1 src/inspector-controls.js:60 -msgid "" -"Sticky posts are removed from the canonical list used to build the sequence." -msgstr "" -"Les publications épinglées sont retirées de la liste canonique utilisée pour " -"construire la séquence." diff --git a/languages/next-posts-block.pot b/languages/next-posts-block.pot index 018cf45..5b33621 100644 --- a/languages/next-posts-block.pot +++ b/languages/next-posts-block.pot @@ -58,18 +58,3 @@ msgstr "" #: src/variation.js:27 msgid "related" msgstr "" - -#: build/index.js:1 -#: src/inspector-controls.js:48 -msgid "Sequential settings" -msgstr "" - -#: build/index.js:1 -#: src/inspector-controls.js:56 -msgid "Exclude sticky posts from the sequence" -msgstr "" - -#: build/index.js:1 -#: src/inspector-controls.js:60 -msgid "Sticky posts are removed from the canonical list used to build the sequence." -msgstr "" diff --git a/next-posts-block.php b/next-posts-block.php index d0d5c1b..121535c 100644 --- a/next-posts-block.php +++ b/next-posts-block.php @@ -2,7 +2,7 @@ /** * Plugin Name: Next Posts — Query Loop Block * Description: Query Loop variation that displays posts sequentially relative to the current post, with wrap-around. Supports ordering by date or title. - * Version: 1.2.2 + * Version: 1.3.0 * Requires at least: 6.1 * Requires PHP: 7.4 * Author: Jensen SIU @@ -20,7 +20,7 @@ exit; } -define('NEXT_POSTS_BLOCK_VERSION', '1.2.2'); +define('NEXT_POSTS_BLOCK_VERSION', '1.3.0'); define('NEXT_POSTS_BLOCK_PATH', plugin_dir_path(__FILE__)); define('NEXT_POSTS_BLOCK_URL', plugin_dir_url(__FILE__)); diff --git a/package.json b/package.json index 28f915d..e1f060b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "next-posts-block", - "version": "1.2.2", + "version": "1.3.0", "description": "Query Loop block variation: N posts following the current post in the chosen order, with wrap-around.", "main": "build/index.js", "private": true, diff --git a/readme.txt b/readme.txt index a84c9ea..2f7a736 100644 --- a/readme.txt +++ b/readme.txt @@ -4,27 +4,30 @@ Tags: query-loop, block, next-post, related-posts, post-navigation Requires at least: 6.1 Tested up to: 6.9 Requires PHP: 7.4 -Stable tag: 1.2.2 +Stable tag: 1.3.0 License: GPL-2.0-or-later License URI: https://www.gnu.org/licenses/gpl-2.0.html -A Query Loop variation that displays the next N posts after the current one, with wrap-around — configurable order, any post type. +A Query Loop variation that anchors N posts to the current one — continues the flow you curate with native filters (taxonomy, author, keyword, sticky modes) and wraps at boundaries. == Description == -Next Posts is a variation of the native Query Loop block. It appears in the Block Editor inserter as "Next Posts". On any single-post page, it renders the N posts that come after the current one in the chosen sort order, with silent wrap-around at list boundaries — so readers always see a full set of suggestions, even when viewing the last post in a sequence. When the block is used outside a singular context (archives, home, search, template previews), it falls back to the first N items of the canonical list in the chosen sort order instead of rendering empty. +Next Posts is a variation of the native Query Loop block. It appears in the Block Editor inserter as "Next Posts". On any single-post page, it renders the N posts that follow the current one in the list you configured, with silent wrap-around at list boundaries — so readers always see a full set of suggestions, even when viewing the last post in a sequence. When the block is used outside a singular context (archives, home, search, template previews), it falls back to the first N items of the list instead of rendering empty. -Because Next Posts reuses the native Query Loop block, authors configure the card template visually with the standard Post blocks (Post Title, Post Featured Image, Post Excerpt, and so on). The block is well suited for post navigation, related posts rails, and any layout that needs a sequential list of entries tied to the current post. +The block is an **anchored continuation of a curated flow**: it honors every native Query Loop filter in the sidebar (taxonomy, author, keyword) and all four sticky modes (Include / Ignore / Exclude / Only), then applies the anchor+wrap on top. The list the block walks is exactly the list your filters describe. + +Because Next Posts reuses the native Query Loop block, authors configure the card template visually with the standard Post blocks (Post Title, Post Featured Image, Post Excerpt, and so on). The block is well suited for post navigation, related-posts rails, and any layout that needs a sequential list of entries tied to the current post. **Key features:** * Works with any public post type +* Honors native Query Loop filters: taxonomy, author, keyword, sticky modes * Visually editable card template using native Post blocks (Post Title, Post Featured Image, Post Excerpt, etc.) * Uses the native Query Loop ordering controls (date / title, ASC / DESC) * Configurable number of items (1–10) * Silent wrap-around at end/start of the list * Non-singular fallback renders the first N items of the canonical list -* Editor preview reflects order changes in real time +* Editor preview reflects filter and order changes in real time * Zero third-party runtime dependencies == Installation == @@ -62,7 +65,7 @@ Yes. The block works with any post type registered as `public`. = Can I exclude sticky posts from the sequence? = -When enabled, sticky posts are removed from the sequence. Sticky posts never inflate the item count beyond the number you configured, regardless of this setting. +Yes — use the native "Sticky posts" control in the block sidebar. Choose "Exclude" to drop stickies, "Only" to limit the sequence to stickies, "Ignore" to keep them in their natural date position, or leave it on the default "Include" mode. Sticky posts never inflate the item count beyond the number you configured, regardless of the mode. == Screenshots == @@ -72,6 +75,12 @@ When enabled, sticky posts are removed from the sequence. Sticky posts never inf == Changelog == += 1.3.0 = +* Added: Native support for taxonomy, author, and keyword filters in the block sidebar — the block now honors every Query Loop filter and applies the anchor+wrap on top. +* Added: Four sticky-posts modes (Include / Ignore / Exclude / Only) matching the native `core/query` behavior. +* Removed: Custom "Exclude sticky posts from the sequence" toggle (replaced by the native Sticky posts control). The native control is now visible and fully wired to the server-side list builder. +* Technical: `CanonicalList::build()` replaces `::get()` and accepts the full native `query` attributes bag; REST preview middleware sends a single JSON `sequential_query_attrs` param for editor-server parity on any filter combination. + = 1.2.2 = * Fix: the block could render more posts than its "Number of posts" setting when sticky posts existed on the site. Sticky-posts neutralization has been restored (it regressed in 1.2.1). * Technical: the pre_render reset was moved below the blockName check, so inner blocks (core/post-template, etc.) no longer wipe the armed state before query_loop_block_query_vars consumes it. Sibling-leak prevention for unrelated core/query blocks is preserved. @@ -99,6 +108,9 @@ When enabled, sticky posts are removed from the sequence. Sticky posts never inf == Upgrade Notice == += 1.3.0 = +Native support for taxonomy, author, and keyword filters, plus four sticky-post modes. The custom "Exclude sticky" toggle is replaced by the native Sticky posts control — existing blocks continue to work; the `excludeSticky` attribute is ignored. + = 1.2.2 = Fix: sticky-posts neutralization (regressed in 1.2.1) is restored. Recommended update if you saw extra posts prepended to the block's output. diff --git a/src/index.js b/src/index.js index 2c8a1cb..1328ddc 100644 --- a/src/index.js +++ b/src/index.js @@ -6,6 +6,5 @@ */ import './variation'; -import './inspector-controls'; import './rest-middleware'; import './lock-custom-query'; diff --git a/src/inspector-controls.js b/src/inspector-controls.js deleted file mode 100644 index 9a5d355..0000000 --- a/src/inspector-controls.js +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Custom InspectorControls panel for the Next Posts variation. - * - * Adds a "Sequential settings" panel with a single ToggleControl: - * "Exclude sticky posts from the sequence" → writes to query.excludeSticky. - * - * Scoped to our variation via a BlockEdit filter. Other core/query blocks - * are left untouched. - * - * Pairs with the server-side CanonicalList::get($post_type, …, $exclude_sticky) - * which rebuilds the list without sticky IDs when the flag is true. - */ - -import { addFilter } from '@wordpress/hooks'; -import { createHigherOrderComponent } from '@wordpress/compose'; -import { InspectorControls } from '@wordpress/block-editor'; -import { PanelBody, ToggleControl } from '@wordpress/components'; -import { Fragment } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - -const NAMESPACE = 'next-posts-block/query'; - -const withSequentialSettings = createHigherOrderComponent( - ( BlockEdit ) => ( props ) => { - const isOurVariation = - props.name === 'core/query' && - props.attributes?.namespace === NAMESPACE; - - if ( ! isOurVariation ) { - return ; - } - - const excludeSticky = Boolean( - props.attributes?.query?.excludeSticky - ); - - const setExcludeSticky = ( value ) => { - props.setAttributes( { - query: { ...props.attributes.query, excludeSticky: value }, - } ); - }; - - return ( - - - - - - - - - ); - }, - 'withSequentialSettings' -); - -addFilter( - 'editor.BlockEdit', - 'next-posts-block/sequential-settings', - withSequentialSettings -); diff --git a/src/lock-custom-query.js b/src/lock-custom-query.js index da61499..aae3fd2 100644 --- a/src/lock-custom-query.js +++ b/src/lock-custom-query.js @@ -1,22 +1,10 @@ /** - * Locks native core/query controls that conflict with Next Posts semantics. + * Locks the native core/query Query Type toggle so our variation stays on + * inherit=false. Native sticky and filter controls are intentionally left + * visible — the server now honors them verbatim (see CanonicalList::build). * - * 1. Query Type toggle (v1.1.0): locked to "Custom" (inherit: false) so the - * plugin's server-side filter cannot be bypassed. - * - Data integrity: effect coerces inherit=true back to false. - * - UI: CSS :has() rule hides the lone ToggleGroupControl ToolsPanelItem. - * - * 2. Sticky posts SelectControl (v1.2.0): hidden and neutralized. Users - * configure sticky exclusion via our own ToggleControl in - * inspector-controls.js (query.excludeSticky). Server always forces - * ignore_sticky_posts=1 and clears post__not_in so the native 4-option - * control has no effect even if it leaks through. - * - UI: MutationObserver + label-based match. Robust across WP versions - * and locales (matches "sticky posts" / "publications épinglées" / - * explicit label fallback). On deselect, hidden items are restored. - * - * Both hides are best-effort on the UI layer; the data layer enforcement - * (inherit=false + ignore_sticky_posts=1) remains authoritative regardless. + * - Data integrity: effect coerces inherit=true back to false. + * - UI: CSS :has() rule hides the lone ToggleGroupControl ToolsPanelItem. */ import { addFilter } from '@wordpress/hooks'; @@ -25,10 +13,6 @@ import { useEffect } from '@wordpress/element'; const NAMESPACE = 'next-posts-block/query'; -/* ------------------------------------------------------------------ */ -/* (1) Query Type toggle lock */ -/* ------------------------------------------------------------------ */ - const QUERY_TYPE_STYLE_ID = 'next-posts-block-hide-query-type'; const QUERY_TYPE_HIDE_RULE = `.components-tools-panel-item:has(.components-toggle-group-control){display:none!important}`; @@ -45,80 +29,6 @@ function removeQueryTypeStyle() { if ( el ) el.remove(); } -/* ------------------------------------------------------------------ */ -/* (2) Sticky posts SelectControl hide */ -/* ------------------------------------------------------------------ */ - -// Label patterns matched against the ToolsPanelItem's own