diff --git a/COMMIT_MESSAGE.txt b/COMMIT_MESSAGE.txt
new file mode 100644
index 0000000..52f14c5
--- /dev/null
+++ b/COMMIT_MESSAGE.txt
@@ -0,0 +1,18 @@
+feat: Add "Searched Term" filter option to taxonomy-text block
+
+Adds support for displaying the current search term from WordPress search
+queries (?s= parameter) in both the standalone taxonomy-text block and the
+RichText inline format.
+
+Changes:
+- Add "search" to filterType enum in block.json
+- Add "Searched Term" option to Filter Source dropdown in editor
+- Extract search term from $_GET['s'] or get_query_var('s')
+- Decode URL-encoded search terms (e.g., "garden+tools" → "garden tools")
+- Hide Value Type and Link controls for search filter (search terms only
+ support prefix/suffix, no description or link options)
+- Update both edit.js (standalone block) and format.js (RichText format)
+
+The search filter will display the searched term when a WordPress search
+is active, allowing dynamic display of search queries in content.
+
diff --git a/PR_UPDATE.txt b/PR_UPDATE.txt
new file mode 100644
index 0000000..3ec2a4b
--- /dev/null
+++ b/PR_UPDATE.txt
@@ -0,0 +1,32 @@
+## Added "Searched Term" Filter Option
+
+This update adds support for displaying the current search term from WordPress search queries in the taxonomy-text block and RichText inline format.
+
+### Features
+
+- **New Filter Source**: "Searched Term" option added to the Filter Source dropdown
+- **URL Parameter Support**: Extracts search term from `?s=` URL parameter (WordPress native search)
+- **URL Decoding**: Automatically decodes URL-encoded search terms (e.g., `garden+tools` → `garden tools`)
+- **Prefix/Suffix Support**: Search terms support prefix and suffix text like other filter types
+- **Simplified Options**: Search filter hides Value Type and Link controls since search terms don't have descriptions or archive links
+
+### Technical Details
+
+- Added `"search"` to `filterType` enum in `block.json`
+- Updated editor components (`edit.js` and `format.js`) to handle search filter type
+- Added search term extraction logic in `get_taxonomy_text_result()` function
+- Search term is extracted from `$_GET['s']` or `get_query_var('s')` and URL-decoded
+- Returns `null` if no search term is present (block won't render)
+
+### Usage
+
+When a user searches on a WordPress site (e.g., `?s=garden+tools`), the taxonomy-text block can now display the searched term dynamically. This is useful for displaying search results context like "Showing results for: garden tools" or similar messaging.
+
+### Files Changed
+
+- `src/taxonomy-text/block.json` - Added "search" to filterType enum
+- `src/taxonomy-text/edit.js` - Added search option and UI controls
+- `src/taxonomy-text/format.js` - Added search option for RichText format
+- `inc/namespace.php` - Added search term extraction logic
+- Built assets updated
+
diff --git a/README.md b/README.md
index f543ef9..e48daca 100644
--- a/README.md
+++ b/README.md
@@ -14,8 +14,9 @@ Easy to use and lightweight, built using the WordPress Interactivity API.
* Add a query block. This can anyhere that the query block is supported e.g. page, template, or pattern.
* Add one of the filter blocks and configure as required:
- * Taxonomy filter. Select which taxonomy to to use, customise the label (and whether it's shown), and customise the text used when none is selected.
+* Taxonomy filter. Select which taxonomy to to use, customise the label (and whether it's shown), and customise the text used when none is selected. Optionally enable **Show Terms in Current Results** to limit the dropdown to terms attached to the posts returned by the surrounding query.
* Post type filter. Customise the label (and whether it's shown), as well as the text used when no filter is applied.
+* Sort filter. Presents the same ordering options available in the Query Loop block (“Order By”) so visitors can toggle between newest/oldest, alphabetical, menu order, etc. Pick which sort choices appear via checkboxes in the block settings.
* Search block. No extra options.

diff --git a/build/.DS_Store b/build/.DS_Store
new file mode 100644
index 0000000..f74352b
Binary files /dev/null and b/build/.DS_Store differ
diff --git a/build/sort/block.json b/build/sort/block.json
new file mode 100644
index 0000000..a3b1990
--- /dev/null
+++ b/build/sort/block.json
@@ -0,0 +1,73 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "query-filter/sort",
+ "version": "0.1.0",
+ "title": "Sort Filter",
+ "category": "theme",
+ "icon": "sort",
+ "description": "Allows visitors to change the order of results within a query loop block.",
+ "ancestor": [
+ "core/query"
+ ],
+ "usesContext": [
+ "queryId",
+ "query"
+ ],
+ "supports": {
+ "html": false,
+ "className": true,
+ "customClassName": true,
+ "color": {
+ "background": true,
+ "text": true
+ },
+ "typography": {
+ "fontSize": true,
+ "textAlign": true,
+ "lineHeight": true,
+ "__experimentalFontFamily": true,
+ "__experimentalFontWeight": true,
+ "__experimentalFontStyle": true,
+ "__experimentalTextTransform": true,
+ "__experimentalTextDecoration": true,
+ "__experimentalLetterSpacing": true,
+ "__experimentalDefaultControls": {
+ "fontSize": true
+ }
+ },
+ "spacing": {
+ "margin": true,
+ "padding": true,
+ "blockGap": true
+ },
+ "interactivity": {
+ "clientNavigation": true
+ }
+ },
+ "attributes": {
+ "label": {
+ "type": "string"
+ },
+ "showLabel": {
+ "type": "boolean",
+ "default": true
+ },
+ "options": {
+ "type": "object",
+ "default": {
+ "date_desc": true,
+ "date_asc": true,
+ "title_asc": true,
+ "title_desc": true,
+ "comment_desc": true,
+ "menu_order": true
+ }
+ }
+ },
+ "textdomain": "query-filter",
+ "editorScript": "file:./index.js",
+ "style": "query-filter-view",
+ "render": "file:./render.php"
+}
+
diff --git a/build/sort/index.asset.php b/build/sort/index.asset.php
new file mode 100644
index 0000000..ce0507c
--- /dev/null
+++ b/build/sort/index.asset.php
@@ -0,0 +1,12 @@
+ [
+ 'wp-block-editor',
+ 'wp-blocks',
+ 'wp-components',
+ 'wp-element',
+ 'wp-i18n',
+ ],
+ 'version' => '1.0.0',
+];
+
diff --git a/build/sort/index.js b/build/sort/index.js
new file mode 100644
index 0000000..555281a
--- /dev/null
+++ b/build/sort/index.js
@@ -0,0 +1,155 @@
+( function ( blocks, blockEditor, components, element, i18n ) {
+ const { registerBlockType } = blocks;
+ const { InspectorControls, useBlockProps } = blockEditor;
+ const {
+ PanelBody,
+ TextControl,
+ ToggleControl,
+ CheckboxControl,
+ } = components;
+ const { Fragment, createElement: el } = element;
+ const { __ } = i18n;
+
+ const SORT_OPTIONS = [
+ {
+ key: 'date_desc',
+ label: __( 'Newest to Oldest', 'query-filter' ),
+ orderby: 'date',
+ order: 'DESC',
+ },
+ {
+ key: 'date_asc',
+ label: __( 'Oldest to Newest', 'query-filter' ),
+ orderby: 'date',
+ order: 'ASC',
+ },
+ {
+ key: 'title_asc',
+ label: __( 'A → Z', 'query-filter' ),
+ orderby: 'title',
+ order: 'ASC',
+ },
+ {
+ key: 'title_desc',
+ label: __( 'Z → A', 'query-filter' ),
+ orderby: 'title',
+ order: 'DESC',
+ },
+ {
+ key: 'comment_desc',
+ label: __( 'Most Commented', 'query-filter' ),
+ orderby: 'comment_count',
+ order: 'DESC',
+ },
+ {
+ key: 'menu_order',
+ label: __( 'Menu Order', 'query-filter' ),
+ orderby: 'menu_order',
+ order: 'ASC',
+ },
+ ];
+
+ const DEFAULT_OPTIONS = SORT_OPTIONS.reduce( ( acc, option ) => {
+ acc[ option.key ] = true;
+ return acc;
+ }, {} );
+
+ registerBlockType( 'query-filter/sort', {
+ edit( { attributes, setAttributes } ) {
+ const { label, showLabel = true, options = {} } = attributes;
+
+ const resolvedOptions = {
+ ...DEFAULT_OPTIONS,
+ ...options,
+ };
+
+ const enabledOptions = SORT_OPTIONS.filter(
+ ( option ) => resolvedOptions[ option.key ]
+ );
+
+ return el(
+ Fragment,
+ null,
+ el(
+ InspectorControls,
+ null,
+ el(
+ PanelBody,
+ { title: __( 'Sort Settings', 'query-filter' ) },
+ el( TextControl, {
+ label: __( 'Label', 'query-filter' ),
+ value: label,
+ help: __(
+ 'If empty then no label will be shown',
+ 'query-filter'
+ ),
+ onChange: ( next ) =>
+ setAttributes( { label: next } ),
+ } ),
+ el( ToggleControl, {
+ label: __( 'Show Label', 'query-filter' ),
+ checked: showLabel,
+ onChange: ( next ) =>
+ setAttributes( { showLabel: next } ),
+ } ),
+ el(
+ 'div',
+ { className: 'wp-block-query-filter-sort__options' },
+ SORT_OPTIONS.map( ( option ) =>
+ el( CheckboxControl, {
+ key: option.key,
+ label: option.label,
+ checked: resolvedOptions[ option.key ],
+ onChange: ( next ) =>
+ setAttributes( {
+ options: {
+ ...resolvedOptions,
+ [ option.key ]: next,
+ },
+ } ),
+ } )
+ )
+ )
+ )
+ ),
+ el(
+ 'div',
+ useBlockProps( { className: 'wp-block-query-filter' } ),
+ showLabel &&
+ el(
+ 'label',
+ {
+ className:
+ 'wp-block-query-filter-sort__label wp-block-query-filter__label',
+ },
+ label || __( 'Order Results', 'query-filter' )
+ ),
+ el(
+ 'select',
+ {
+ className:
+ 'wp-block-query-filter-sort__select wp-block-query-filter__select',
+ disabled: true,
+ },
+ enabledOptions.map( ( option ) =>
+ el(
+ 'option',
+ {
+ key: option.key,
+ },
+ option.label
+ )
+ )
+ )
+ )
+ );
+ },
+ } );
+} )(
+ window.wp.blocks,
+ window.wp.blockEditor,
+ window.wp.components,
+ window.wp.element,
+ window.wp.i18n
+);
+
diff --git a/build/sort/render.php b/build/sort/render.php
new file mode 100644
index 0000000..a044d8b
--- /dev/null
+++ b/build/sort/render.php
@@ -0,0 +1,108 @@
+context['query'] ) ) {
+ return;
+}
+
+wp_enqueue_script_module( 'query-filter-taxonomy-view-script-module' );
+
+$id = 'query-filter-' . wp_generate_uuid4();
+$label = $attributes['label'] ?? __( 'Order Results', 'query-filter' );
+$show_label = $attributes['showLabel'] ?? true;
+
+$sort_options = [
+ 'date_desc' => [
+ 'label' => __( 'Newest to Oldest', 'query-filter' ),
+ 'orderby' => 'date',
+ 'order' => 'DESC',
+ ],
+ 'date_asc' => [
+ 'label' => __( 'Oldest to Newest', 'query-filter' ),
+ 'orderby' => 'date',
+ 'order' => 'ASC',
+ ],
+ 'title_asc' => [
+ 'label' => __( 'A → Z', 'query-filter' ),
+ 'orderby' => 'title',
+ 'order' => 'ASC',
+ ],
+ 'title_desc' => [
+ 'label' => __( 'Z → A', 'query-filter' ),
+ 'orderby' => 'title',
+ 'order' => 'DESC',
+ ],
+ 'comment_desc' => [
+ 'label' => __( 'Most Commented', 'query-filter' ),
+ 'orderby' => 'comment_count',
+ 'order' => 'DESC',
+ ],
+ 'menu_order' => [
+ 'label' => __( 'Menu Order', 'query-filter' ),
+ 'orderby' => 'menu_order',
+ 'order' => 'ASC',
+ ],
+];
+
+$enabled = array_merge(
+ array_fill_keys( array_keys( $sort_options ), true ),
+ is_array( $attributes['options'] ?? null ) ? $attributes['options'] : []
+);
+
+$target_query_id = empty( $block->context['query']['inherit'] )
+ ? (string) ( $block->context['queryId'] ?? 0 )
+ : 'main';
+
+$orderby_var = 'query-post_orderby';
+$page_var = 'query-page';
+$base_url = remove_query_arg( [ $orderby_var, $page_var, 'query-post_id', 'query-id' ] );
+
+if ( ! empty( $block->context['query']['inherit'] ) ) {
+ $current_paged = (int) get_query_var( 'paged' );
+ if ( $current_paged > 1 ) {
+ $base_url = str_replace( '/page/' . $current_paged, '', $base_url );
+ }
+ $base_url = remove_query_arg( [ 'page' ], $base_url );
+}
+
+$base_url = add_query_arg(
+ [
+ 'query-post_id' => $target_query_id,
+ ],
+ $base_url
+);
+$base_url = HM\Query_Loop_Filter\normalize_query_filter_url( $base_url );
+
+if ( empty( array_filter( $enabled ) ) ) {
+ return;
+}
+
+$current_selection = sanitize_text_field( wp_unslash( $_GET[ $orderby_var ] ?? '' ) );
+
+?>
+
'wp-block-query-filter' ] ); ?> data-wp-interactive="query-filter" data-wp-context="{}">
+
+
+
+
diff --git a/build/taxonomy-text/block.json b/build/taxonomy-text/block.json
new file mode 100644
index 0000000..8ec1270
--- /dev/null
+++ b/build/taxonomy-text/block.json
@@ -0,0 +1,85 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "query-filter/taxonomy-text",
+ "version": "0.1.0",
+ "title": "Taxonomy Text",
+ "category": "theme",
+ "icon": "feedback",
+ "description": "Outputs the currently selected taxonomy term or sort option for the surrounding query loop.",
+ "keywords": [
+ "query",
+ "filter",
+ "taxonomy",
+ "seo"
+ ],
+ "ancestor": [
+ "core/query"
+ ],
+ "usesContext": [
+ "queryId",
+ "query"
+ ],
+ "supports": {
+ "html": false,
+ "className": true,
+ "customClassName": true,
+ "color": {
+ "text": true,
+ "background": true
+ },
+ "typography": {
+ "fontSize": true,
+ "lineHeight": true,
+ "__experimentalFontWeight": true,
+ "__experimentalTextTransform": true,
+ "__experimentalLetterSpacing": true
+ },
+ "spacing": {
+ "margin": true,
+ "padding": true
+ }
+ },
+ "attributes": {
+ "filterType": {
+ "type": "string",
+ "enum": [
+ "tag",
+ "category",
+ "sort",
+ "page",
+ "yoast_primary_category",
+ "search"
+ ],
+ "default": "tag"
+ },
+ "valueType": {
+ "type": "string",
+ "enum": [
+ "title",
+ "description",
+ "page"
+ ],
+ "default": "title"
+ },
+ "link": {
+ "type": "boolean",
+ "default": false
+ },
+ "showAfterFirstPage": {
+ "type": "boolean",
+ "default": true
+ },
+ "prefix": {
+ "type": "string",
+ "default": ""
+ },
+ "suffix": {
+ "type": "string",
+ "default": ""
+ }
+ },
+ "textdomain": "query-filter",
+ "editorScript": "file:./index.js",
+ "render": "file:./render.php"
+}
\ No newline at end of file
diff --git a/build/taxonomy-text/index.asset.php b/build/taxonomy-text/index.asset.php
new file mode 100644
index 0000000..7885030
--- /dev/null
+++ b/build/taxonomy-text/index.asset.php
@@ -0,0 +1 @@
+ array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-rich-text'), 'version' => '5d3efbe6c3765b6828b4');
diff --git a/build/taxonomy-text/index.js b/build/taxonomy-text/index.js
new file mode 100644
index 0000000..ad6bd25
--- /dev/null
+++ b/build/taxonomy-text/index.js
@@ -0,0 +1 @@
+(()=>{"use strict";const e=window.wp.blocks,t=JSON.parse('{"UU":"query-filter/taxonomy-text"}'),r=window.wp.i18n,l=window.wp.blockEditor,a=window.wp.components,i=window.ReactJSXRuntime,o=[{label:(0,r.__)("Tag","query-filter"),value:"tag"},{label:(0,r.__)("Category","query-filter"),value:"category"},{label:(0,r.__)("Sort","query-filter"),value:"sort"},{label:(0,r.__)("Page Number","query-filter"),value:"page"},{label:(0,r.__)("Yoast Primary Category","query-filter"),value:"yoast_primary_category"},{label:(0,r.__)("Searched Term","query-filter"),value:"search"}],s=[{label:(0,r.__)("Title","query-filter"),value:"title"},{label:(0,r.__)("Description","query-filter"),value:"description"},{label:(0,r.__)("Page Number","query-filter"),value:"page"}],n=window.wp.richText,u=window.wp.primitives;var y=(0,i.jsx)(u.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,i.jsx)(u.Path,{d:"M17.5 4v5a2 2 0 0 1-2 2h-7a2 2 0 0 1-2-2V4H8v5a.5.5 0 0 0 .5.5h7A.5.5 0 0 0 16 9V4h1.5Zm0 16v-5a2 2 0 0 0-2-2h-7a2 2 0 0 0-2 2v5H8v-5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v5h1.5Z"})});const f=window.wp.element,p="query-filter/taxonomy-inline-text",c="data-query-filter-text",_={filterType:"tag",valueType:"title",prefix:"",suffix:"",link:!1,showAfterFirstPage:!0},g=[{label:(0,r.__)("Tag","query-filter"),value:"tag"},{label:(0,r.__)("Category","query-filter"),value:"category"},{label:(0,r.__)("Sort","query-filter"),value:"sort"},{label:(0,r.__)("Page Number","query-filter"),value:"page"},{label:(0,r.__)("Yoast Primary Category","query-filter"),value:"yoast_primary_category"},{label:(0,r.__)("Searched Term","query-filter"),value:"search"}],x=[{label:(0,r.__)("Title","query-filter"),value:"title"},{label:(0,r.__)("Description","query-filter"),value:"description"},{label:(0,r.__)("Page Number","query-filter"),value:"page"}],T=(e,t)=>e.find((e=>e.value===t))?.label||t;(0,n.registerFormatType)(p,{title:(0,r.__)("Taxonomy Text","query-filter"),tagName:"span",className:"taxonomy-text-inline",attributes:{[c]:c},edit:({value:e,onChange:t,isActive:o,activeAttributes:s})=>{const u=(0,f.useMemo)((()=>(e=>{if(!e)return _;try{var t,r;const l=JSON.parse(e);return{filterType:l.filterType||_.filterType,valueType:l.valueType||_.valueType,prefix:l.prefix||"",suffix:l.suffix||"",link:null!==(t=l.link)&&void 0!==t?t:_.link,showAfterFirstPage:null!==(r=l.showAfterFirstPage)&&void 0!==r?r:_.showAfterFirstPage}}catch(e){return _}})(s?.[c])),[s?.[c]]),[v,h]=(0,f.useState)(!1),[d,b]=(0,f.useState)(u);(0,f.useEffect)((()=>{b(u)}),[u]),(0,f.useEffect)((()=>{"page"===d.filterType&&"page"!==d.valueType?b((e=>({...e,valueType:"page"}))):"sort"!==d.filterType&&"search"!==d.filterType||"description"!==d.valueType||b((e=>({...e,valueType:"title"})))}),[d.filterType,d.valueType]);const q="page"===d.filterType?x.filter((e=>"page"===e.value)):"sort"===d.filterType||"search"===d.filterType?x.filter((e=>"title"===e.value)):x;return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(l.RichTextToolbarButton,{icon:y,title:(0,r.__)("Taxonomy Text","query-filter"),onClick:()=>h(!0),isActive:o}),v&&(0,i.jsxs)(a.Modal,{title:(0,r.__)("Dynamic Taxonomy Text","query-filter"),onRequestClose:()=>h(!1),children:[(0,i.jsx)(a.SelectControl,{label:(0,r.__)("Filter Source","query-filter"),value:d.filterType,options:g,onChange:e=>{b((t=>({...t,filterType:e,valueType:"page"===e?"page":"sort"!==e&&"search"!==e||"description"!==t.valueType?t.valueType:"title"})))}}),"search"!==d.filterType&&(0,i.jsx)(a.SelectControl,{label:(0,r.__)("Value Type","query-filter"),value:d.valueType,options:q,disabled:"page"===d.filterType,onChange:e=>b((t=>({...t,valueType:e})))}),(0,i.jsx)(a.TextControl,{label:(0,r.__)("Prefix Text","query-filter"),value:d.prefix,onChange:e=>b((t=>({...t,prefix:e})))}),(0,i.jsx)(a.TextControl,{label:(0,r.__)("Suffix Text","query-filter"),value:d.suffix,onChange:e=>b((t=>({...t,suffix:e})))}),["tag","category","yoast_primary_category"].includes(d.filterType)&&(0,i.jsx)(a.ToggleControl,{label:(0,r.__)("Link to term archive","query-filter"),checked:!!d.link,onChange:e=>b((t=>({...t,link:e})))}),"page"===d.filterType&&(0,i.jsx)(a.ToggleControl,{label:(0,r.__)("Only show after page 1","query-filter"),checked:!!d.showAfterFirstPage,onChange:e=>b((t=>({...t,showAfterFirstPage:e})))}),(0,i.jsx)(a.Flex,{justify:"flex-start",children:(0,i.jsx)(a.FlexItem,{children:(0,i.jsx)(a.Button,{variant:"primary",onClick:()=>{const l={[c]:JSON.stringify(d)},a=e.start===e.end;let i=e;if(a&&!o){const e=(e=>{const t=[(0,r.__)("taxonomy","query-filter"),T(g,e.filterType).toLowerCase()],l="sort"!==e.filterType&&"search"!==e.filterType||"page"===e.valueType?T(x,e.valueType):(0,r.__)("title","query-filter");return t.push(l.toLowerCase()),t.join(" ")})(d);i=(0,n.insert)(i,e);const t=i.start,l=t-e.length;i={...i,start:l,end:t}}const s=(0,n.applyFormat)(i,{type:p,attributes:l});t(s),h(!1)},children:(0,r.__)("Apply","query-filter")})})})]})]})}}),(0,e.registerBlockType)(t.UU,{edit:function({attributes:e,setAttributes:t}){const{filterType:n="tag",valueType:u="title",prefix:y="",suffix:f="",link:p=!1,showAfterFirstPage:c=!0}=e,_=(0,l.useBlockProps)({className:`taxonomy-text taxonomy-text--${n}`}),g="sort"===n?(0,r.__)("Selected Sort","query-filter"):"search"===n?(0,r.__)("Searched Term","query-filter"):"description"===u?(0,r.__)("Selected Term Description","query-filter"):(0,r.__)("Selected Term","query-filter"),x="page"===n?s.filter((e=>"page"===e.value)):"sort"===n||"search"===n?s.filter((e=>"title"===e.value)):s;return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(l.InspectorControls,{children:(0,i.jsxs)(a.PanelBody,{title:(0,r.__)("Display Settings","query-filter"),children:[(0,i.jsx)(a.SelectControl,{label:(0,r.__)("Filter Source","query-filter"),value:n,options:o,onChange:e=>{t({filterType:e,valueType:"page"===e?"page":"sort"!==e&&"search"!==e||"description"!==u?u:"title"})}}),["tag","category","yoast_primary_category"].includes(n)&&(0,i.jsx)(a.ToggleControl,{label:(0,r.__)("Link to term archive","query-filter"),checked:!!p,onChange:e=>t({link:e})}),"search"!==n&&(0,i.jsx)(a.SelectControl,{label:(0,r.__)("Value Type","query-filter"),value:u,options:x,disabled:"page"===n,onChange:e=>t({valueType:e})}),"page"===n&&(0,i.jsx)(a.ToggleControl,{label:(0,r.__)("Only show after page 1","query-filter"),checked:!!c,onChange:e=>t({showAfterFirstPage:e})}),(0,i.jsx)(a.TextControl,{label:(0,r.__)("Prefix Text","query-filter"),value:y,onChange:e=>t({prefix:e}),placeholder:""}),(0,i.jsx)(a.TextControl,{label:(0,r.__)("Suffix Text","query-filter"),value:f,onChange:e=>t({suffix:e}),placeholder:""})]})}),(0,i.jsx)("span",{..._,children:`${y||""}${g}${f||""}`})]})}})})();
\ No newline at end of file
diff --git a/build/taxonomy-text/render.php b/build/taxonomy-text/render.php
new file mode 100644
index 0000000..871a64f
--- /dev/null
+++ b/build/taxonomy-text/render.php
@@ -0,0 +1,43 @@
+ $block->context['query'] ?? [],
+ 'queryId' => $block->context['queryId'] ?? null,
+ ]
+);
+
+if ( is_null( $result ) ) {
+ return '';
+}
+
+$link_url = ( ! empty( $attributes['link'] ) && ! empty( $result['url'] ) )
+ ? $result['url']
+ : '';
+
+$wrapper_attributes = get_block_wrapper_attributes(
+ [
+ 'class' => sprintf(
+ 'taxonomy-text taxonomy-text--%s',
+ sanitize_html_class( $result['filter_type'] )
+ ),
+ ]
+);
+
+?>
+>
+
+
+
+
+
+
+
+
+
diff --git a/build/taxonomy/block.json b/build/taxonomy/block.json
index 6cdb93d..275ef8b 100644
--- a/build/taxonomy/block.json
+++ b/build/taxonomy/block.json
@@ -6,7 +6,7 @@
"title": "Taxonomy Filter",
"category": "theme",
"icon": "filter",
- "description": "Allows users to filter by taxonomy terms when placed wihin a query loop block",
+ "description": "Allows users to filter by taxonomy terms when placed within a query loop block",
"ancestor": [
"core/query"
],
@@ -59,6 +59,10 @@
"showLabel": {
"type": "boolean",
"default": true
+ },
+ "limitToCurrentResults": {
+ "type": "boolean",
+ "default": false
}
},
"textdomain": "query-filter",
diff --git a/build/taxonomy/index-rtl.css b/build/taxonomy/index-rtl.css
index 39bc4f5..8b13789 100644
--- a/build/taxonomy/index-rtl.css
+++ b/build/taxonomy/index-rtl.css
@@ -1 +1 @@
-@view-transition{navigation:auto}.wp-block-query-filter{display:flex;flex-direction:column;justify-content:stretch}
+
diff --git a/build/taxonomy/index.asset.php b/build/taxonomy/index.asset.php
index 4b3ef34..398e51c 100644
--- a/build/taxonomy/index.asset.php
+++ b/build/taxonomy/index.asset.php
@@ -1 +1 @@
- array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-i18n'), 'version' => 'f1456d24ac8e3da497aa');
+ array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-i18n'), 'version' => '579d7a7dcf9fe19ab7b2');
diff --git a/build/taxonomy/index.css b/build/taxonomy/index.css
index 39bc4f5..8b13789 100644
--- a/build/taxonomy/index.css
+++ b/build/taxonomy/index.css
@@ -1 +1 @@
-@view-transition{navigation:auto}.wp-block-query-filter{display:flex;flex-direction:column;justify-content:stretch}
+
diff --git a/build/taxonomy/index.js b/build/taxonomy/index.js
index 5868836..2fb0d5a 100644
--- a/build/taxonomy/index.js
+++ b/build/taxonomy/index.js
@@ -1 +1 @@
-(()=>{"use strict";const e=window.wp.blocks,l=window.wp.i18n,t=window.wp.blockEditor,o=window.wp.components,n=window.wp.data,r=window.ReactJSXRuntime,a=JSON.parse('{"UU":"query-filter/taxonomy"}');(0,e.registerBlockType)(a.UU,{edit:function({attributes:e,setAttributes:a}){const{taxonomy:i,emptyLabel:s,label:c,showLabel:u}=e,b=(0,n.useSelect)((e=>{const l=(e("core").getTaxonomies({per_page:100})||[]).filter((e=>e.visibility.publicly_queryable));return l&&l.length>0&&!i&&a({taxonomy:l[0].slug,label:l[0].name}),l}),[i]),y=(0,n.useSelect)((e=>e("core").getEntityRecords("taxonomy",i,{number:50})||[]),[i]);return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.InspectorControls,{children:(0,r.jsxs)(o.PanelBody,{title:(0,l.__)("Taxonomy Settings","query-filter"),children:[(0,r.jsx)(o.SelectControl,{label:(0,l.__)("Select Taxonomy","query-filter"),value:i,options:(b||[]).map((e=>({label:e.name,value:e.slug}))),onChange:e=>a({taxonomy:e,label:b.find((l=>l.slug===e)).name})}),(0,r.jsx)(o.TextControl,{label:(0,l.__)("Label","query-filter"),value:c,help:(0,l.__)("If empty then no label will be shown","query-filter"),onChange:e=>a({label:e})}),(0,r.jsx)(o.ToggleControl,{label:(0,l.__)("Show Label","query-filter"),checked:u,onChange:e=>a({showLabel:e})}),(0,r.jsx)(o.TextControl,{label:(0,l.__)("Empty Choice Label","query-filter"),value:s,placeholder:(0,l.__)("All","query-filter"),onChange:e=>a({emptyLabel:e})})]})}),(0,r.jsxs)("div",{...(0,t.useBlockProps)({className:"wp-block-query-filter"}),children:[u&&(0,r.jsx)("label",{className:"wp-block-query-filter-taxonomy__label wp-block-query-filter__label",children:c}),(0,r.jsxs)("select",{className:"wp-block-query-filter-taxonomy__select wp-block-query-filter__select",inert:!0,children:[(0,r.jsx)("option",{children:s||(0,l.__)("All","query-filter")}),y.map((e=>(0,r.jsx)("option",{children:e.name},e.slug)))]})]})]})}})})();
\ No newline at end of file
+(()=>{"use strict";const e=window.wp.blocks,l=JSON.parse('{"UU":"query-filter/taxonomy"}'),t=window.wp.i18n,o=window.wp.blockEditor,n=window.wp.components,r=window.wp.data,s=window.ReactJSXRuntime;(0,e.registerBlockType)(l.UU,{edit:function({attributes:e,setAttributes:l}){const{taxonomy:a,emptyLabel:i,label:c,showLabel:u,limitToCurrentResults:y}=e,b=(0,r.useSelect)((e=>{const t=(e("core").getTaxonomies({per_page:100})||[]).filter((e=>e.visibility?.publicly_queryable));return t&&t.length>0&&!a&&l({taxonomy:t[0].slug,label:t[0].name}),t}),[a]),m=(0,r.useSelect)((e=>e("core").getEntityRecords("taxonomy",a,{number:50,hide_empty:!0})||[]),[a]),_=(0,o.useBlockProps)({className:"wp-block-query-filter"});return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(o.InspectorControls,{children:(0,s.jsxs)(n.PanelBody,{title:(0,t.__)("Taxonomy Settings","query-filter"),children:[(0,s.jsx)(n.SelectControl,{label:(0,t.__)("Select Taxonomy","query-filter"),value:a,options:(b||[]).map((e=>({label:e.name,value:e.slug}))),onChange:e=>{const t=b.find((l=>l.slug===e));l({taxonomy:e,label:t?.name||c})}}),(0,s.jsx)(n.TextControl,{label:(0,t.__)("Label","query-filter"),value:c,help:(0,t.__)("If empty then no label will be shown","query-filter"),onChange:e=>l({label:e})}),(0,s.jsx)(n.ToggleControl,{label:(0,t.__)("Show Label","query-filter"),checked:u,onChange:e=>l({showLabel:e})}),(0,s.jsx)(n.TextControl,{label:(0,t.__)("Empty Choice Label","query-filter"),value:i,placeholder:(0,t.__)("All","query-filter"),onChange:e=>l({emptyLabel:e})}),(0,s.jsx)(n.ToggleControl,{label:(0,t.__)("Only show terms in current results","query-filter"),checked:!!y,onChange:e=>l({limitToCurrentResults:e})})]})}),(0,s.jsxs)("div",{..._,children:[u&&(0,s.jsx)("label",{className:"wp-block-query-filter-taxonomy__label wp-block-query-filter__label",children:c||(0,t.__)("Taxonomy","query-filter")}),(0,s.jsxs)("select",{className:"wp-block-query-filter-taxonomy__select wp-block-query-filter__select",inert:!0,children:[(0,s.jsx)("option",{children:i||(0,t.__)("All","query-filter")}),m.map((e=>(0,s.jsx)("option",{children:e.name},e.slug)))]})]})]})}})})();
\ No newline at end of file
diff --git a/build/taxonomy/render.php b/build/taxonomy/render.php
index 97258b4..e4e349e 100644
--- a/build/taxonomy/render.php
+++ b/build/taxonomy/render.php
@@ -4,27 +4,63 @@
}
$id = 'query-filter-' . wp_generate_uuid4();
-
$taxonomy = get_taxonomy( $attributes['taxonomy'] );
-if ( empty( $block->context['query']['inherit'] ) ) {
- $query_id = $block->context['queryId'] ?? 0;
- $query_var = sprintf( 'query-%d-%s', $query_id, $attributes['taxonomy'] );
- $page_var = isset( $block->context['queryId'] ) ? 'query-' . $block->context['queryId'] . '-page' : 'query-page';
- $base_url = remove_query_arg( [ $query_var, $page_var ] );
-} else {
- $query_var = sprintf( 'query-%s', $attributes['taxonomy'] );
- $page_var = 'page';
- $base_url = str_replace( '/page/' . get_query_var( 'paged' ), '', remove_query_arg( [ $query_var, $page_var ] ) );
+$target_query_id = empty( $block->context['query']['inherit'] )
+ ? (string) ( $block->context['queryId'] ?? 0 )
+ : 'main';
+
+$query_var = sprintf( 'query-%s', $attributes['taxonomy'] );
+$page_var = 'query-page';
+$base_url = remove_query_arg( [ $query_var, $page_var, 'query-post_id', 'query-id' ] );
+
+if ( ! empty( $block->context['query']['inherit'] ) ) {
+ $current_paged = (int) get_query_var( 'paged' );
+ if ( $current_paged > 1 ) {
+ $base_url = str_replace( '/page/' . $current_paged, '', $base_url );
+ }
+ $base_url = remove_query_arg( [ 'page' ], $base_url );
}
-$terms = get_terms( [
- 'hide_empty' => true,
- 'taxonomy' => $attributes['taxonomy'],
- 'number' => 100,
-] );
+$base_url = add_query_arg(
+ [
+ 'query-post_id' => $target_query_id,
+ ],
+ $base_url
+);
+$base_url = HM\Query_Loop_Filter\normalize_query_filter_url( $base_url );
+
+$terms = [];
+
+if ( ! empty( $attributes['limitToCurrentResults'] ) ) {
+ $post_ids = HM\Query_Loop_Filter\get_query_loop_post_ids_for_block( $block );
+
+ if ( ! empty( $post_ids ) ) {
+ $terms = wp_get_object_terms(
+ $post_ids,
+ $attributes['taxonomy'],
+ [
+ 'orderby' => 'name',
+ 'order' => 'ASC',
+ 'number' => 100,
+ ]
+ );
+ }
-if ( is_wp_error( $terms ) || empty( $terms ) ) {
+ if ( is_wp_error( $terms ) ) {
+ return;
+ }
+}
+
+if ( empty( $terms ) ) {
+ $terms = get_terms( [
+ 'hide_empty' => true,
+ 'taxonomy' => $attributes['taxonomy'],
+ 'number' => 100,
+ ] );
+}
+
+if ( is_wp_error( $terms ) || ( empty( $terms ) && empty( $attributes['limitToCurrentResults'] ) ) ) {
return;
}
?>
@@ -35,8 +71,20 @@
+
diff --git a/inc/namespace.php b/inc/namespace.php
index 204a02a..85acf8d 100644
--- a/inc/namespace.php
+++ b/inc/namespace.php
@@ -19,6 +19,11 @@ function bootstrap() : void {
// General hooks.
add_filter( 'query_loop_block_query_vars', __NAMESPACE__ . '\\filter_query_loop_block_query_vars', 10, 3 );
add_action( 'pre_get_posts', __NAMESPACE__ . '\\pre_get_posts_transpose_query_vars' );
+ add_action( 'template_redirect', __NAMESPACE__ . '\\maybe_redirect_taxonomy_query_page' );
+ add_action( 'enqueue_block_editor_assets', __NAMESPACE__ . '\\enqueue_block_editor_assets' );
+ add_filter( 'render_block', __NAMESPACE__ . '\\filter_inline_taxonomy_text_in_block', 10, 2 );
+ add_filter( 'the_content', __NAMESPACE__ . '\\filter_inline_taxonomy_text_in_content', 12 );
+ add_filter( 'the_posts', __NAMESPACE__ . '\\store_query_loop_posts', 10, 2 );
add_filter( 'block_type_metadata', __NAMESPACE__ . '\\filter_block_type_metadata', 10 );
add_action( 'init', __NAMESPACE__ . '\\register_blocks' );
add_action( 'enqueue_block_assets', __NAMESPACE__ . '\\action_wp_enqueue_scripts' );
@@ -45,6 +50,29 @@ function action_wp_enqueue_scripts() : void {
);
}
+/**
+ * Enqueue editor-only assets for block/format features.
+ *
+ * @return void
+ */
+function enqueue_block_editor_assets() : void {
+ $asset_path = ROOT_DIR . '/build/taxonomy-text/index.asset.php';
+
+ if ( ! file_exists( $asset_path ) ) {
+ return;
+ }
+
+ $asset = include $asset_path;
+
+ wp_enqueue_script(
+ 'query-filter-taxonomy-text',
+ plugins_url( '/build/taxonomy-text/index.js', PLUGIN_FILE ),
+ $asset['dependencies'],
+ $asset['version'],
+ true
+ );
+}
+
/**
* Fires after WordPress has finished loading but before any headers are sent.
*
@@ -52,6 +80,8 @@ function action_wp_enqueue_scripts() : void {
function register_blocks() : void {
register_block_type( ROOT_DIR . '/build/taxonomy' );
register_block_type( ROOT_DIR . '/build/post-type' );
+ register_block_type( ROOT_DIR . '/build/sort' );
+ register_block_type( ROOT_DIR . '/build/taxonomy-text' );
}
/**
@@ -82,11 +112,42 @@ function pre_get_posts_transpose_query_vars( WP_Query $query ) : void {
return;
}
- $prefix = $query->is_main_query() ? 'query-' : "query-{$query_id}-";
+ $current_query_identifier = $query->is_main_query() ? 'main' : (string) $query_id;
+ $requested_query_id = sanitize_text_field(
+ wp_unslash(
+ $_GET['query-post_id']
+ ?? ( $_GET['query-id'] ?? '' )
+ )
+ );
+ $legacy_prefix = $query->is_main_query() ? 'query-' : "query-{$query_id}-";
+ $use_legacy_params = false;
+
+ if ( 'main' !== $current_query_identifier && empty( $requested_query_id ) ) {
+ foreach ( array_keys( $_GET ) as $key ) {
+ if ( strpos( $key, $legacy_prefix ) === 0 ) {
+ $use_legacy_params = true;
+ break;
+ }
+ }
+ }
+
+ if ( ! $use_legacy_params ) {
+ if ( 'main' !== $current_query_identifier && $requested_query_id !== $current_query_identifier ) {
+ return;
+ }
+
+ if ( 'main' === $current_query_identifier && $requested_query_id && 'main' !== $requested_query_id ) {
+ return;
+ }
+ }
+
$tax_query = [];
+ $page_param_handled = false;
$valid_keys = [
'post_type' => $query->is_search() ? 'any' : 'post',
's' => '',
+ 'orderby' => '',
+ 'order' => '',
];
// Preserve valid params for later retrieval.
@@ -98,31 +159,127 @@ function pre_get_posts_transpose_query_vars( WP_Query $query ) : void {
}
// Map get params to this query.
- foreach ( $_GET as $key => $value ) {
- if ( strpos( $key, $prefix ) === 0 ) {
- $key = str_replace( $prefix, '', $key );
+ if ( $use_legacy_params ) {
+ foreach ( $_GET as $key => $value ) {
+ if ( strpos( $key, $legacy_prefix ) !== 0 ) {
+ continue;
+ }
+
+ $param = str_replace( $legacy_prefix, '', $key );
+ $value = sanitize_text_field( urldecode( wp_unslash( $value ) ) );
+
+ if ( 'page' === $param ) {
+ $paged = max( 1, absint( $value ) );
+ $query->set( 'paged', $paged );
+ $page_param_handled = true;
+ continue;
+ }
+
+ if ( 'post_orderby' === $param ) {
+ $parts = explode( ':', $value );
+ $orderby = sanitize_key( $parts[0] ?? '' );
+ $order = strtoupper( sanitize_text_field( $parts[1] ?? '' ) );
+
+ if ( ! empty( $orderby ) ) {
+ $query->set( 'orderby', $orderby );
+ }
+
+ if ( in_array( $order, [ 'ASC', 'DESC' ], true ) ) {
+ $query->set( 'order', $order );
+ }
+
+ continue;
+ }
+
+ if ( get_taxonomy( $param ) ) {
+ $tax_query['relation'] = 'AND';
+ $tax_query[] = [
+ 'taxonomy' => $param,
+ 'terms' => [ $value ],
+ 'field' => 'slug',
+ ];
+ continue;
+ }
+
+ $param = sanitize_key( $param );
+
+ if ( ! array_key_exists( $param, $valid_keys ) ) {
+ continue;
+ }
+
+ $query->set(
+ $param,
+ $value
+ );
+ }
+ } else {
+ foreach ( $_GET as $key => $value ) {
+ if ( strpos( $key, 'query-' ) !== 0 || in_array( $key, [ 'query-post_id', 'query-id' ], true ) ) {
+ continue;
+ }
+
$value = sanitize_text_field( urldecode( wp_unslash( $value ) ) );
+ $param = substr( $key, 6 );
- // Handle taxonomies specifically.
- if ( get_taxonomy( $key ) ) {
+ if ( 'page' === $param ) {
+ $paged = max( 1, absint( $value ) );
+ $query->set( 'paged', $paged );
+ $page_param_handled = true;
+ continue;
+ }
+
+ if ( get_taxonomy( $param ) ) {
$tax_query['relation'] = 'AND';
$tax_query[] = [
- 'taxonomy' => $key,
+ 'taxonomy' => $param,
'terms' => [ $value ],
'field' => 'slug',
];
- } else {
- // Other options should map directly to query vars.
- $key = sanitize_key( $key );
+ continue;
+ }
+
+ if ( 'post_orderby' === $param ) {
+ $parts = explode( ':', $value );
+ $orderby = sanitize_key( $parts[0] ?? '' );
+ $order = strtoupper( sanitize_text_field( $parts[1] ?? '' ) );
- if ( ! in_array( $key, array_keys( $valid_keys ), true ) ) {
- continue;
+ if ( ! empty( $orderby ) ) {
+ $query->set( 'orderby', $orderby );
}
- $query->set(
- $key,
- $value
- );
+ if ( in_array( $order, [ 'ASC', 'DESC' ], true ) ) {
+ $query->set( 'order', $order );
+ }
+
+ continue;
+ }
+
+ // Other options should map directly to query vars.
+ $param = sanitize_key( $param );
+
+ if ( ! array_key_exists( $param, $valid_keys ) ) {
+ continue;
+ }
+
+ $query->set(
+ $param,
+ $value
+ );
+ }
+ }
+
+ if ( ! $page_param_handled ) {
+ $path_paged = (int) get_query_var( 'paged', 0 );
+
+ if ( $path_paged > 1 ) {
+ $should_use_path_paged = ( 'main' === $current_query_identifier );
+
+ if ( ! $should_use_path_paged && ! $use_legacy_params ) {
+ $should_use_path_paged = (string) $current_query_identifier === $requested_query_id;
+ }
+
+ if ( $should_use_path_paged && ! (int) $query->get( 'paged' ) ) {
+ $query->set( 'paged', $path_paged );
}
}
}
@@ -172,11 +329,27 @@ function render_block_search( string $block_content, array $block, \WP_Block $in
wp_enqueue_script_module( 'query-filter-taxonomy-view-script-module' );
- $query_var = empty( $instance->context['query']['inherit'] )
- ? sprintf( 'query-%d-s', $instance->context['queryId'] ?? 0 )
- : 's';
+ $target_query_id = empty( $instance->context['query']['inherit'] )
+ ? (string) ( $instance->context['queryId'] ?? 0 )
+ : 'main';
+ $query_var = 'query-s';
+
+ $action = remove_query_arg( [ $query_var, 'query-page', 'query-post_id', 'query-id' ] );
+ if ( ! empty( $instance->context['query']['inherit'] ) ) {
+ $current_paged = (int) get_query_var( 'paged', 1 );
+ if ( $current_paged > 1 ) {
+ $action = str_replace( '/page/' . $current_paged, '', $action );
+ }
+ $action = remove_query_arg( [ 'page' ], $action );
+ }
- $action = str_replace( '/page/'. get_query_var( 'paged', 1 ), '', add_query_arg( [ $query_var => '' ] ) );
+ $action = add_query_arg(
+ [
+ 'query-post_id' => $target_query_id,
+ $query_var => '',
+ ],
+ $action
+ );
// Note sanitize_text_field trims whitespace from start/end of string causing unexpected behaviour.
$value = wp_unslash( $_GET[ $query_var ] ?? '' );
@@ -222,3 +395,506 @@ function render_block_query( $block_content, $block ) {
return (string) $block_content;
}
+
+/**
+ * Cache the post IDs for each rendered query loop.
+ *
+ * @param array $posts Array of post objects.
+ * @param WP_Query $query Current WP_Query object.
+ * @return array
+ */
+function store_query_loop_posts( array $posts, WP_Query $query ) : array {
+ $query_id = $query->get( 'query_id', null );
+
+ if ( is_null( $query_id ) ) {
+ return $posts;
+ }
+
+ $cache = &query_loop_posts_cache();
+ $cache[ $query_id ] = array_map( 'intval', wp_list_pluck( $posts, 'ID' ) );
+
+ return $posts;
+}
+
+/**
+ * Retrieve the post IDs associated with the provided block's query.
+ *
+ * @param \WP_Block $block Block instance.
+ * @return array
+ */
+function get_query_loop_post_ids_for_block( \WP_Block $block ) : array {
+ if ( empty( $block->context['query'] ) ) {
+ return [];
+ }
+
+ if ( ! empty( $block->context['query']['inherit'] ) ) {
+ global $wp_query;
+
+ return array_map( 'intval', wp_list_pluck( $wp_query->posts ?? [], 'ID' ) );
+ }
+
+ $query_id = $block->context['queryId'] ?? null;
+
+ if ( is_null( $query_id ) ) {
+ return [];
+ }
+
+ $cache = &query_loop_posts_cache();
+
+ return $cache[ $query_id ] ?? [];
+}
+
+/**
+ * Helper to store post IDs for each query loop.
+ *
+ * @return array>
+ */
+function &query_loop_posts_cache() : array {
+ if ( ! isset( $GLOBALS['hm_query_loop_filter_query_posts'] ) || ! is_array( $GLOBALS['hm_query_loop_filter_query_posts'] ) ) {
+ $GLOBALS['hm_query_loop_filter_query_posts'] = [];
+ }
+
+ return $GLOBALS['hm_query_loop_filter_query_posts'];
+}
+
+/**
+ * Ensure certain query parameters remain readable in URLs.
+ *
+ * @param string $url URL to normalize.
+ * @return string
+ */
+function normalize_query_filter_url( string $url ) : string {
+ if ( false === strpos( $url, 'query-post_orderby=' ) ) {
+ return $url;
+ }
+
+ return preg_replace_callback(
+ '/(query-post_orderby=)([^]+)/',
+ static function ( $matches ) {
+ return $matches[1] . rawurldecode( $matches[2] );
+ },
+ $url
+ );
+}
+
+/**
+ * Redirect taxonomy pagination requests to pretty permalinks.
+ *
+ * @return void
+ */
+function maybe_redirect_taxonomy_query_page() : void {
+ if ( is_admin() || wp_doing_ajax() ) {
+ return;
+ }
+
+ if ( ! ( is_category() || is_tag() || is_tax() ) ) {
+ return;
+ }
+
+ $page_param = null;
+
+ if ( isset( $_GET['query-page'] ) ) {
+ $page_param = 'query-page';
+ } else {
+ foreach ( array_keys( $_GET ) as $key ) {
+ if ( preg_match( '/^query-\d+-page$/', $key ) ) {
+ $page_param = $key;
+ break;
+ }
+ }
+ }
+
+ if ( is_null( $page_param ) ) {
+ return;
+ }
+
+ $page = max( 1, absint( wp_unslash( $_GET[ $page_param ] ) ) );
+
+ if ( $page <= 1 ) {
+ return;
+ }
+
+ $destination = get_pagenum_link( $page );
+
+ if ( empty( $destination ) ) {
+ return;
+ }
+
+ $params = [];
+
+ foreach ( $_GET as $key => $value ) {
+ if ( $key === $page_param || strpos( $key, 'query-' ) !== 0 ) {
+ continue;
+ }
+
+ $params[ $key ] = sanitize_text_field( wp_unslash( $value ) );
+ }
+
+ if ( ! empty( $params ) ) {
+ $destination = add_query_arg( $params, $destination );
+ }
+
+ wp_safe_redirect( $destination, 301 );
+ exit;
+}
+
+/**
+ * Build the rendered taxonomy text result for the block/format.
+ *
+ * @param array $attributes Attributes.
+ * @param array $context Block context.
+ * @return array{ text: string, filter_type: string, url: string }|null
+ */
+function get_taxonomy_text_result( array $attributes, array $context = [] ) : ?array {
+ $allowed_filters = [ 'tag', 'category', 'sort', 'yoast_primary_category', 'search' ];
+ $filter_type = $attributes['filterType'] ?? 'tag';
+ $value_type = $attributes['valueType'] ?? 'title';
+
+ if ( ! in_array( $filter_type, $allowed_filters, true ) ) {
+ $filter_type = 'tag';
+ }
+
+ if ( ! in_array( $value_type, [ 'title', 'description', 'page' ], true ) ) {
+ $value_type = 'title';
+ }
+
+ $prefix = (string) ( $attributes['prefix'] ?? '' );
+ $suffix = (string) ( $attributes['suffix'] ?? '' );
+ $query_context = $context['query'] ?? [];
+ $inherits_main = empty( $context ) || ! empty( $query_context['inherit'] );
+ $target_query_id = $inherits_main ? 'main' : (string) ( $context['queryId'] ?? 0 );
+
+ $requested_query_id = sanitize_text_field(
+ wp_unslash(
+ $_GET['query-post_id']
+ ?? ( $_GET['query-id'] ?? '' )
+ )
+ );
+
+ $legacy_prefix = 'main' === $target_query_id ? 'query-' : "query-{$target_query_id}-";
+ $use_legacy_params = false;
+
+ if ( 'main' !== $target_query_id && empty( $requested_query_id ) ) {
+ foreach ( array_keys( $_GET ) as $key ) {
+ if ( strpos( $key, $legacy_prefix ) === 0 ) {
+ $use_legacy_params = true;
+ break;
+ }
+ }
+ }
+
+ if ( ! $use_legacy_params ) {
+ if ( 'main' !== $target_query_id && $requested_query_id !== $target_query_id ) {
+ return null;
+ }
+
+ if ( 'main' === $target_query_id && $requested_query_id && 'main' !== $requested_query_id ) {
+ return null;
+ }
+ }
+
+ $param_suffix_map = [
+ 'tag' => 'post_tag',
+ 'category' => 'category',
+ 'sort' => 'post_orderby',
+ ];
+
+ $show_after_first_page = isset( $attributes['showAfterFirstPage'] )
+ ? (bool) $attributes['showAfterFirstPage']
+ : true;
+
+ $display_value = '';
+ $raw_value = '';
+ $url = '';
+
+ // Special-case: Yoast primary category, if available.
+ if ( 'yoast_primary_category' === $filter_type ) {
+ if ( ! class_exists( '\WPSEO_Primary_Term' ) ) {
+ return null;
+ }
+
+ $post_id = $context['postId'] ?? get_the_ID();
+
+ if ( ! $post_id ) {
+ return null;
+ }
+
+ $primary = new \WPSEO_Primary_Term( 'category', $post_id );
+ $term_id = $primary->get_primary_term();
+
+ if ( ! $term_id || is_wp_error( $term_id ) ) {
+ return null;
+ }
+
+ $term = get_term( (int) $term_id, 'category' );
+
+ if ( ! $term || is_wp_error( $term ) ) {
+ return null;
+ }
+
+ $title = $term->name;
+ $description = trim( wp_strip_all_tags( $term->description ?? '' ) );
+
+ if ( 'description' === $value_type && '' !== $description ) {
+ $display_value = $description;
+ } else {
+ $display_value = $title;
+ }
+
+ $term_link = get_term_link( $term );
+
+ if ( ! is_wp_error( $term_link ) ) {
+ $url = (string) $term_link;
+ }
+ } elseif ( 'search' === $filter_type ) {
+ // Search term mode – extract from URL parameter 's'.
+ $search_term = get_query_var( 's', '' );
+
+ if ( empty( $search_term ) && isset( $_GET['s'] ) ) {
+ $search_term = sanitize_text_field( wp_unslash( $_GET['s'] ) );
+ }
+
+ if ( empty( $search_term ) ) {
+ return null;
+ }
+
+ // Decode URL-encoded search terms (e.g., "garden+tools" -> "garden tools").
+ $display_value = urldecode( $search_term );
+ } elseif ( 'page' === $value_type ) {
+ // Page mode – just derive from pagination.
+ $paged = 1;
+
+ // Prefer explicit query-* page params for the targeted query.
+ foreach ( array_keys( $_GET ) as $key ) {
+ if ( 'main' === $target_query_id && 'query-page' === $key ) {
+ $paged = max( 1, absint( wp_unslash( $_GET[ $key ] ) ) );
+ break;
+ }
+
+ if (
+ 'main' !== $target_query_id
+ && preg_match( '/^query-(\d+)-page$/', $key, $matches )
+ && (string) $matches[1] === (string) $target_query_id
+ ) {
+ $paged = max( 1, absint( wp_unslash( $_GET[ $key ] ) ) );
+ break;
+ }
+ }
+
+ if ( $paged <= 1 ) {
+ $paged = (int) get_query_var( 'paged', 1 );
+ }
+
+ if ( $paged <= 1 && $show_after_first_page ) {
+ return null;
+ }
+
+ $display_value = (string) max( 1, $paged );
+ } else {
+ // For tag, category and sort, resolve the raw query param value first.
+ $param_candidates = array_unique(
+ [
+ 'query-' . $param_suffix_map[ $filter_type ],
+ $legacy_prefix . $param_suffix_map[ $filter_type ],
+ ]
+ );
+
+ foreach ( $param_candidates as $param_name ) {
+ if ( isset( $_GET[ $param_name ] ) && '' !== $_GET[ $param_name ] ) {
+ $raw_value = sanitize_text_field( urldecode( wp_unslash( $_GET[ $param_name ] ) ) );
+ break;
+ }
+ }
+
+ if ( '' === $raw_value ) {
+ return null;
+ }
+
+ if ( 'sort' === $filter_type ) {
+ $sort_labels = [
+ 'date:DESC' => __( 'Newest to Oldest', 'query-filter' ),
+ 'date:ASC' => __( 'Oldest to Newest', 'query-filter' ),
+ 'title:ASC' => __( 'A → Z', 'query-filter' ),
+ 'title:DESC' => __( 'Z → A', 'query-filter' ),
+ 'comment_count:DESC' => __( 'Most Commented', 'query-filter' ),
+ 'menu_order:ASC' => __( 'Menu Order', 'query-filter' ),
+ ];
+
+ $parts = explode( ':', $raw_value );
+ $orderby = sanitize_key( $parts[0] ?? '' );
+ $order = strtoupper( sanitize_text_field( $parts[1] ?? '' ) );
+ $normalized = $orderby . ':' . $order;
+
+ if ( isset( $sort_labels[ $normalized ] ) ) {
+ $display_value = $sort_labels[ $normalized ];
+ }
+
+ // Ensure we never try to look up a "description" label for sort.
+ if ( 'description' === $value_type ) {
+ $value_type = 'title';
+ }
+ } else {
+ $taxonomy = 'tag' === $filter_type ? 'post_tag' : 'category';
+
+ $raw_slugs = array_filter(
+ array_map(
+ 'trim',
+ explode( ',', $raw_value )
+ )
+ );
+
+ $slugs = array_values(
+ array_filter(
+ array_map(
+ static function ( $slug ) {
+ $sanitized = sanitize_title( $slug );
+
+ return '' === $sanitized ? null : $sanitized;
+ },
+ $raw_slugs
+ )
+ )
+ );
+
+ if ( empty( $slugs ) ) {
+ return null;
+ }
+
+ $terms = get_terms(
+ [
+ 'taxonomy' => $taxonomy,
+ 'slug' => $slugs,
+ 'hide_empty' => false,
+ ]
+ );
+
+ if ( is_wp_error( $terms ) || empty( $terms ) ) {
+ return null;
+ }
+
+ $term_lookup = [];
+
+ foreach ( $terms as $term ) {
+ $term_lookup[ $term->slug ] = [
+ 'title' => $term->name,
+ 'description' => trim( wp_strip_all_tags( $term->description ?? '' ) ),
+ ];
+ }
+
+ $ordered_values = [];
+ $value_key = 'description' === $value_type ? 'description' : 'title';
+
+ foreach ( $slugs as $slug ) {
+ if ( isset( $term_lookup[ $slug ][ $value_key ] ) && '' !== $term_lookup[ $slug ][ $value_key ] ) {
+ $ordered_values[] = $term_lookup[ $slug ][ $value_key ];
+ }
+ }
+
+ if ( empty( $ordered_values ) ) {
+ return null;
+ }
+
+ $display_value = implode( ', ', $ordered_values );
+ }
+ }
+
+ if ( '' === $display_value ) {
+ return null;
+ }
+
+ $text = $prefix . $display_value . $suffix;
+
+ if ( '' === trim( $text ) ) {
+ return null;
+ }
+
+ return [
+ 'text' => $text,
+ 'filter_type' => $filter_type,
+ 'url' => $url,
+ ];
+}
+
+/**
+ * Replace inline taxonomy spans in rendered content.
+ *
+ * @param string $content Content string.
+ * @param array $context Block context.
+ * @return string
+ */
+function replace_inline_taxonomy_text_spans( string $content, array $context = [] ) : string {
+ if ( false === strpos( $content, 'data-query-filter-text=' ) ) {
+ return $content;
+ }
+
+ $pattern = '/]*)data-query-filter-text="([^"]+)"([^>]*)>(.*?)<\/span>/si';
+
+ return preg_replace_callback(
+ $pattern,
+ static function ( $matches ) use ( $context ) {
+ $decoded = html_entity_decode( $matches[2], ENT_QUOTES, 'UTF-8' );
+ $attributes = json_decode( $decoded, true );
+
+ if ( ! is_array( $attributes ) ) {
+ return $matches[0];
+ }
+
+ $result = get_taxonomy_text_result( $attributes, $context );
+
+ if ( is_null( $result ) ) {
+ return '';
+ }
+
+ $class = sprintf(
+ 'taxonomy-text taxonomy-text--%s',
+ sanitize_html_class( $result['filter_type'] )
+ );
+
+ $link_url = ( ! empty( $attributes['link'] ) && ! empty( $result['url'] ) )
+ ? $result['url']
+ : '';
+
+ $text_html = esc_html( $result['text'] );
+
+ if ( $link_url ) {
+ $text_html = sprintf(
+ '%s',
+ esc_url( $link_url ),
+ $text_html
+ );
+ }
+
+ return sprintf(
+ '%s',
+ esc_attr( $class ),
+ $text_html
+ );
+ },
+ $content
+ );
+}
+
+/**
+ * Filter the rendered block content to inject inline taxonomy text.
+ *
+ * @param string $block_content Block HTML.
+ * @param array $block Parsed block data.
+ * @return string
+ */
+function filter_inline_taxonomy_text_in_block( string $block_content, array $block ) : string {
+ if ( false === strpos( $block_content, 'data-query-filter-text=' ) ) {
+ return $block_content;
+ }
+
+ return replace_inline_taxonomy_text_spans( $block_content, $block['context'] ?? [] );
+}
+
+/**
+ * Fallback for legacy content where render_block is not invoked.
+ *
+ * @param string $content Post content.
+ * @return string
+ */
+function filter_inline_taxonomy_text_in_content( string $content ) : string {
+ return replace_inline_taxonomy_text_spans( $content );
+}
diff --git a/package-lock.json b/package-lock.json
index 6e6203b..e9db18e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,6 +7,7 @@
"name": "query-loop-filter",
"license": "GPL-2.0-or-later",
"dependencies": {
+ "@wordpress/icons": "^11.3.0",
"@wordpress/scripts": "^28.6.0"
}
},
@@ -3276,6 +3277,11 @@
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="
},
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="
+ },
"node_modules/@types/qs": {
"version": "6.9.15",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz",
@@ -3286,6 +3292,23 @@
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="
},
+ "node_modules/@types/react": {
+ "version": "18.3.27",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
+ "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
"node_modules/@types/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
@@ -3906,6 +3929,41 @@
"@playwright/test": ">=1"
}
},
+ "node_modules/@wordpress/element": {
+ "version": "6.36.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-6.36.0.tgz",
+ "integrity": "sha512-6Ym/Ucik49skz1XJ2GRXENoMjJx7EYnY+fbfor9KtChiCd9/3H4/rI4sZgewVPIO//fCKEk7G30HoR+xB7GZMQ==",
+ "dependencies": {
+ "@types/react": "^18.2.79",
+ "@types/react-dom": "^18.2.25",
+ "@wordpress/escape-html": "^3.36.0",
+ "change-case": "^4.1.2",
+ "is-plain-object": "^5.0.0",
+ "react": "^18.3.0",
+ "react-dom": "^18.3.0"
+ },
+ "engines": {
+ "node": ">=18.12.0",
+ "npm": ">=8.19.2"
+ }
+ },
+ "node_modules/@wordpress/element/node_modules/is-plain-object": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@wordpress/escape-html": {
+ "version": "3.36.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-3.36.0.tgz",
+ "integrity": "sha512-0FvvlVPv+7X8lX5ExcTh6ib/xckGIuVXdnHglR3rZC1MJI682cx4JRUR0Igk6nKyPS8UiSQCKtN3U1aSPtZaCg==",
+ "engines": {
+ "node": ">=18.12.0",
+ "npm": ">=8.19.2"
+ }
+ },
"node_modules/@wordpress/eslint-plugin": {
"version": "20.3.0",
"resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-20.3.0.tgz",
@@ -3988,6 +4046,22 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@wordpress/icons": {
+ "version": "11.3.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-11.3.0.tgz",
+ "integrity": "sha512-ortBmmzg0855houBuJyR1v1WgKjUiPzBTGUbzOAEeNlOoisXaQO0hLIL7L8vFcd2L46i8PQHPvwpL5ifFTSC+g==",
+ "dependencies": {
+ "@wordpress/element": "^6.36.0",
+ "@wordpress/primitives": "^4.36.0"
+ },
+ "engines": {
+ "node": ">=18.12.0",
+ "npm": ">=8.19.2"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0"
+ }
+ },
"node_modules/@wordpress/jest-console": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-8.7.0.tgz",
@@ -4061,6 +4135,22 @@
"prettier": ">=3"
}
},
+ "node_modules/@wordpress/primitives": {
+ "version": "4.36.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/primitives/-/primitives-4.36.0.tgz",
+ "integrity": "sha512-Y/5Vkd/oAFNVe/dJpnpfnJ/ar4uWorCD0DAWglpFktNSAJfD3cqVbchJ6zNzqwXG9lR+7yqeh7ZHsmp2juoy3Q==",
+ "dependencies": {
+ "@wordpress/element": "^6.36.0",
+ "clsx": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=18.12.0",
+ "npm": ">=8.19.2"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0"
+ }
+ },
"node_modules/@wordpress/scripts": {
"version": "28.6.0",
"resolved": "https://registry.npmjs.org/@wordpress/scripts/-/scripts-28.6.0.tgz",
@@ -5460,6 +5550,14 @@
"node": ">=0.10.0"
}
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -6109,6 +6207,11 @@
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="
},
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
+ },
"node_modules/cwd": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/cwd/-/cwd-0.10.0.tgz",
@@ -13504,7 +13607,6 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
- "peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -14116,7 +14218,6 @@
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
- "peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
}
diff --git a/package.json b/package.json
index 97bda5d..8cc369a 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"author": "Human Made Limited",
"license": "GPL-2.0-or-later",
"dependencies": {
+ "@wordpress/icons": "^11.3.0",
"@wordpress/scripts": "^28.6.0"
}
}
diff --git a/src/taxonomy-text/block.json b/src/taxonomy-text/block.json
new file mode 100644
index 0000000..6888d8b
--- /dev/null
+++ b/src/taxonomy-text/block.json
@@ -0,0 +1,65 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "query-filter/taxonomy-text",
+ "version": "0.1.0",
+ "title": "Taxonomy Text",
+ "category": "theme",
+ "icon": "feedback",
+ "description": "Outputs the currently selected taxonomy term or sort option for the surrounding query loop.",
+ "keywords": [ "query", "filter", "taxonomy", "seo" ],
+ "ancestor": [ "core/query" ],
+ "usesContext": [ "queryId", "query" ],
+ "supports": {
+ "html": false,
+ "className": true,
+ "customClassName": true,
+ "color": {
+ "text": true,
+ "background": true
+ },
+ "typography": {
+ "fontSize": true,
+ "lineHeight": true,
+ "__experimentalFontWeight": true,
+ "__experimentalTextTransform": true,
+ "__experimentalLetterSpacing": true
+ },
+ "spacing": {
+ "margin": true,
+ "padding": true
+ }
+ },
+ "attributes": {
+ "filterType": {
+ "type": "string",
+ "enum": [ "tag", "category", "sort", "page", "yoast_primary_category", "search" ],
+ "default": "tag"
+ },
+ "valueType": {
+ "type": "string",
+ "enum": [ "title", "description", "page" ],
+ "default": "title"
+ },
+ "link": {
+ "type": "boolean",
+ "default": false
+ },
+ "showAfterFirstPage": {
+ "type": "boolean",
+ "default": true
+ },
+ "prefix": {
+ "type": "string",
+ "default": ""
+ },
+ "suffix": {
+ "type": "string",
+ "default": ""
+ }
+ },
+ "textdomain": "query-filter",
+ "editorScript": "file:./index.js",
+ "render": "file:./render.php"
+}
+
diff --git a/src/taxonomy-text/edit.js b/src/taxonomy-text/edit.js
new file mode 100644
index 0000000..c659093
--- /dev/null
+++ b/src/taxonomy-text/edit.js
@@ -0,0 +1,137 @@
+import { __ } from '@wordpress/i18n';
+import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
+import {
+ PanelBody,
+ SelectControl,
+ TextControl,
+ ToggleControl,
+} from '@wordpress/components';
+
+const FILTER_OPTIONS = [
+ { label: __( 'Tag', 'query-filter' ), value: 'tag' },
+ { label: __( 'Category', 'query-filter' ), value: 'category' },
+ { label: __( 'Sort', 'query-filter' ), value: 'sort' },
+ { label: __( 'Page Number', 'query-filter' ), value: 'page' },
+ {
+ label: __( 'Yoast Primary Category', 'query-filter' ),
+ value: 'yoast_primary_category',
+ },
+ { label: __( 'Searched Term', 'query-filter' ), value: 'search' },
+];
+
+const VALUE_TYPE_OPTIONS = [
+ { label: __( 'Title', 'query-filter' ), value: 'title' },
+ { label: __( 'Description', 'query-filter' ), value: 'description' },
+ { label: __( 'Page Number', 'query-filter' ), value: 'page' },
+];
+
+export default function Edit( { attributes, setAttributes } ) {
+ const {
+ filterType = 'tag',
+ valueType = 'title',
+ prefix = '',
+ suffix = '',
+ link = false,
+ showAfterFirstPage = true,
+ } = attributes;
+
+ const blockProps = useBlockProps( {
+ className: `taxonomy-text taxonomy-text--${ filterType }`,
+ } );
+
+ const previewValue =
+ filterType === 'sort'
+ ? __( 'Selected Sort', 'query-filter' )
+ : filterType === 'search'
+ ? __( 'Searched Term', 'query-filter' )
+ : valueType === 'description'
+ ? __( 'Selected Term Description', 'query-filter' )
+ : __( 'Selected Term', 'query-filter' );
+
+ const handleFilterChange = ( nextFilter ) => {
+ setAttributes( {
+ filterType: nextFilter,
+ valueType:
+ nextFilter === 'page'
+ ? 'page'
+ : ( nextFilter === 'sort' || nextFilter === 'search' ) && valueType === 'description'
+ ? 'title'
+ : valueType,
+ } );
+ };
+
+ const valueTypeOptions =
+ filterType === 'page'
+ ? VALUE_TYPE_OPTIONS.filter( ( option ) => option.value === 'page' )
+ : filterType === 'sort' || filterType === 'search'
+ ? VALUE_TYPE_OPTIONS.filter( ( option ) => option.value === 'title' )
+ : VALUE_TYPE_OPTIONS;
+
+ return (
+ <>
+
+
+
+ { [ 'tag', 'category', 'yoast_primary_category' ].includes(
+ filterType
+ ) && (
+
+ setAttributes( { link: value } )
+ }
+ />
+ ) }
+ { filterType !== 'search' && (
+
+ setAttributes( { valueType: nextValue } )
+ }
+ />
+ ) }
+ { filterType === 'page' && (
+
+ setAttributes( { showAfterFirstPage: value } )
+ }
+ />
+ ) }
+ setAttributes( { prefix: value } ) }
+ placeholder=""
+ />
+ setAttributes( { suffix: value } ) }
+ placeholder=""
+ />
+
+
+
+ { `${ prefix || '' }${ previewValue }${ suffix || '' }` }
+
+ >
+ );
+}
+
diff --git a/src/taxonomy-text/format.js b/src/taxonomy-text/format.js
new file mode 100644
index 0000000..adf5457
--- /dev/null
+++ b/src/taxonomy-text/format.js
@@ -0,0 +1,261 @@
+import { __ } from '@wordpress/i18n';
+import {
+ registerFormatType,
+ insert,
+ applyFormat,
+} from '@wordpress/rich-text';
+import { RichTextToolbarButton } from '@wordpress/block-editor';
+import {
+ Button,
+ Flex,
+ FlexItem,
+ Modal,
+ SelectControl,
+ TextControl,
+ ToggleControl,
+} from '@wordpress/components';
+import { stack } from '@wordpress/icons';
+import { useEffect, useMemo, useState } from '@wordpress/element';
+
+const FORMAT_NAME = 'query-filter/taxonomy-inline-text';
+const ATTRIBUTE_KEY = 'data-query-filter-text';
+
+const DEFAULT_SETTINGS = {
+ filterType: 'tag',
+ valueType: 'title',
+ prefix: '',
+ suffix: '',
+ link: false,
+ showAfterFirstPage: true,
+};
+
+const FILTER_OPTIONS = [
+ { label: __( 'Tag', 'query-filter' ), value: 'tag' },
+ { label: __( 'Category', 'query-filter' ), value: 'category' },
+ { label: __( 'Sort', 'query-filter' ), value: 'sort' },
+ { label: __( 'Page Number', 'query-filter' ), value: 'page' },
+ {
+ label: __( 'Yoast Primary Category', 'query-filter' ),
+ value: 'yoast_primary_category',
+ },
+ { label: __( 'Searched Term', 'query-filter' ), value: 'search' },
+];
+
+const VALUE_TYPE_OPTIONS = [
+ { label: __( 'Title', 'query-filter' ), value: 'title' },
+ { label: __( 'Description', 'query-filter' ), value: 'description' },
+ { label: __( 'Page Number', 'query-filter' ), value: 'page' },
+];
+
+const getOptionLabel = ( options, value ) =>
+ options.find( ( option ) => option.value === value )?.label || value;
+
+const getPlaceholderText = ( settings ) => {
+ const parts = [
+ __( 'taxonomy', 'query-filter' ),
+ getOptionLabel( FILTER_OPTIONS, settings.filterType ).toLowerCase(),
+ ];
+
+ const valueLabel =
+ ( settings.filterType === 'sort' || settings.filterType === 'search' ) && settings.valueType !== 'page'
+ ? __( 'title', 'query-filter' )
+ : getOptionLabel( VALUE_TYPE_OPTIONS, settings.valueType );
+
+ parts.push( valueLabel.toLowerCase() );
+
+ return parts.join( ' ' );
+};
+
+const parseSettings = ( attributeValue ) => {
+ if ( ! attributeValue ) {
+ return DEFAULT_SETTINGS;
+ }
+
+ try {
+ const parsed = JSON.parse( attributeValue );
+
+ return {
+ filterType: parsed.filterType || DEFAULT_SETTINGS.filterType,
+ valueType: parsed.valueType || DEFAULT_SETTINGS.valueType,
+ prefix: parsed.prefix || '',
+ suffix: parsed.suffix || '',
+ link: parsed.link ?? DEFAULT_SETTINGS.link,
+ showAfterFirstPage:
+ parsed.showAfterFirstPage ?? DEFAULT_SETTINGS.showAfterFirstPage,
+ };
+ } catch ( error ) {
+ return DEFAULT_SETTINGS;
+ }
+};
+
+const FormatEdit = ( {
+ value,
+ onChange,
+ isActive,
+ activeAttributes,
+} ) => {
+ const currentSettings = useMemo(
+ () => parseSettings( activeAttributes?.[ ATTRIBUTE_KEY ] ),
+ [ activeAttributes?.[ ATTRIBUTE_KEY ] ]
+ );
+ const [ isOpen, setIsOpen ] = useState( false );
+ const [ settings, setSettings ] = useState( currentSettings );
+
+ useEffect( () => {
+ setSettings( currentSettings );
+ }, [ currentSettings ] );
+
+ useEffect( () => {
+ if ( settings.filterType === 'page' && settings.valueType !== 'page' ) {
+ setSettings( ( prev ) => ( { ...prev, valueType: 'page' } ) );
+ } else if (
+ ( settings.filterType === 'sort' || settings.filterType === 'search' ) &&
+ settings.valueType === 'description'
+ ) {
+ setSettings( ( prev ) => ( { ...prev, valueType: 'title' } ) );
+ }
+ }, [ settings.filterType, settings.valueType ] );
+
+ const applyFormatToSelection = () => {
+ const attributes = {
+ [ ATTRIBUTE_KEY ]: JSON.stringify( settings ),
+ };
+ const selectionCollapsed = value.start === value.end;
+ let nextValue = value;
+
+ if ( selectionCollapsed && ! isActive ) {
+ const placeholder = getPlaceholderText( settings );
+ nextValue = insert( nextValue, placeholder );
+ const end = nextValue.start;
+ const start = end - placeholder.length;
+ nextValue = { ...nextValue, start, end };
+ }
+
+ const newValue = applyFormat( nextValue, {
+ type: FORMAT_NAME,
+ attributes,
+ } );
+
+ onChange( newValue );
+ setIsOpen( false );
+ };
+
+ const handleFilterChange = ( filterType ) => {
+ setSettings( ( prev ) => ( {
+ ...prev,
+ filterType,
+ valueType:
+ filterType === 'page'
+ ? 'page'
+ : ( filterType === 'sort' || filterType === 'search' ) && prev.valueType === 'description'
+ ? 'title'
+ : prev.valueType,
+ } ) );
+ };
+
+ const valueTypeOptions =
+ settings.filterType === 'page'
+ ? VALUE_TYPE_OPTIONS.filter( ( option ) => option.value === 'page' )
+ : settings.filterType === 'sort' || settings.filterType === 'search'
+ ? VALUE_TYPE_OPTIONS.filter( ( option ) => option.value === 'title' )
+ : VALUE_TYPE_OPTIONS;
+
+ return (
+ <>
+ setIsOpen( true ) }
+ isActive={ isActive }
+ />
+ { isOpen && (
+ setIsOpen( false ) }
+ >
+
+ { settings.filterType !== 'search' && (
+
+ setSettings( ( prev ) => ( {
+ ...prev,
+ valueType,
+ } ) )
+ }
+ />
+ ) }
+
+ setSettings( ( prev ) => ( { ...prev, prefix } ) )
+ }
+ />
+
+ setSettings( ( prev ) => ( { ...prev, suffix } ) )
+ }
+ />
+ { [ 'tag', 'category', 'yoast_primary_category' ].includes(
+ settings.filterType
+ ) && (
+
+ setSettings( ( prev ) => ( {
+ ...prev,
+ link: next,
+ } ) )
+ }
+ />
+ ) }
+ { settings.filterType === 'page' && (
+
+ setSettings( ( prev ) => ( {
+ ...prev,
+ showAfterFirstPage: next,
+ } ) )
+ }
+ />
+ ) }
+
+
+
+
+
+
+ ) }
+ >
+ );
+};
+
+registerFormatType( FORMAT_NAME, {
+ title: __( 'Taxonomy Text', 'query-filter' ),
+ tagName: 'span',
+ className: 'taxonomy-text-inline',
+ attributes: {
+ [ ATTRIBUTE_KEY ]: ATTRIBUTE_KEY,
+ },
+ edit: FormatEdit,
+} );
+
diff --git a/src/taxonomy-text/index.js b/src/taxonomy-text/index.js
new file mode 100644
index 0000000..1945ce8
--- /dev/null
+++ b/src/taxonomy-text/index.js
@@ -0,0 +1,9 @@
+import { registerBlockType } from '@wordpress/blocks';
+import metadata from './block.json';
+import Edit from './edit';
+import './format';
+
+registerBlockType( metadata.name, {
+ edit: Edit,
+} );
+
diff --git a/src/taxonomy-text/render.php b/src/taxonomy-text/render.php
new file mode 100644
index 0000000..871a64f
--- /dev/null
+++ b/src/taxonomy-text/render.php
@@ -0,0 +1,43 @@
+ $block->context['query'] ?? [],
+ 'queryId' => $block->context['queryId'] ?? null,
+ ]
+);
+
+if ( is_null( $result ) ) {
+ return '';
+}
+
+$link_url = ( ! empty( $attributes['link'] ) && ! empty( $result['url'] ) )
+ ? $result['url']
+ : '';
+
+$wrapper_attributes = get_block_wrapper_attributes(
+ [
+ 'class' => sprintf(
+ 'taxonomy-text taxonomy-text--%s',
+ sanitize_html_class( $result['filter_type'] )
+ ),
+ ]
+);
+
+?>
+>
+
+
+
+
+
+
+
+
+
diff --git a/src/taxonomy/block.json b/src/taxonomy/block.json
index 1f1a331..6d1c1ea 100644
--- a/src/taxonomy/block.json
+++ b/src/taxonomy/block.json
@@ -6,7 +6,7 @@
"title": "Taxonomy Filter",
"category": "theme",
"icon": "filter",
- "description": "Allows users to filter by taxonomy terms when placed wihin a query loop block",
+ "description": "Allows users to filter by taxonomy terms when placed within a query loop block",
"ancestor": [ "core/query" ],
"usesContext": [ "queryId", "query" ],
"supports": {
@@ -54,6 +54,10 @@
"showLabel": {
"type": "boolean",
"default": true
+ },
+ "limitToCurrentResults": {
+ "type": "boolean",
+ "default": false
}
},
"textdomain": "query-filter",
@@ -62,3 +66,4 @@
"viewScriptModule": "file:./view.js",
"render": "file:./render.php"
}
+
diff --git a/src/taxonomy/edit.js b/src/taxonomy/edit.js
index 5562901..573dddb 100644
--- a/src/taxonomy/edit.js
+++ b/src/taxonomy/edit.js
@@ -1,5 +1,5 @@
import { __ } from '@wordpress/i18n';
-import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
+import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import {
PanelBody,
SelectControl,
@@ -9,13 +9,19 @@ import {
import { useSelect } from '@wordpress/data';
export default function Edit( { attributes, setAttributes } ) {
- const { taxonomy, emptyLabel, label, showLabel } = attributes;
+ const {
+ taxonomy,
+ emptyLabel,
+ label,
+ showLabel,
+ limitToCurrentResults,
+ } = attributes;
const taxonomies = useSelect(
( select ) => {
const results = (
select( 'core' ).getTaxonomies( { per_page: 100 } ) || []
- ).filter( ( taxonomy ) => taxonomy.visibility.publicly_queryable );
+ ).filter( ( tax ) => tax.visibility?.publicly_queryable );
if ( results && results.length > 0 && ! taxonomy ) {
setAttributes( {
@@ -30,16 +36,18 @@ export default function Edit( { attributes, setAttributes } ) {
);
const terms = useSelect(
- ( select ) => {
- return (
- select( 'core' ).getEntityRecords( 'taxonomy', taxonomy, {
- number: 50,
- } ) || []
- );
- },
+ ( select ) =>
+ select( 'core' ).getEntityRecords( 'taxonomy', taxonomy, {
+ number: 50,
+ hide_empty: true,
+ } ) || [],
[ taxonomy ]
);
+ const blockProps = useBlockProps( {
+ className: 'wp-block-query-filter',
+ } );
+
return (
<>
@@ -47,18 +55,19 @@ export default function Edit( { attributes, setAttributes } ) {
( {
- label: taxonomy.name,
- value: taxonomy.slug,
+ options={ ( taxonomies || [] ).map( ( tax ) => ( {
+ label: tax.name,
+ value: tax.slug,
} ) ) }
- onChange={ ( taxonomy ) =>
+ onChange={ ( value ) => {
+ const selected = taxonomies.find(
+ ( tax ) => tax.slug === value
+ );
setAttributes( {
- taxonomy,
- label: taxonomies.find(
- ( tax ) => tax.slug === taxonomy
- ).name,
- } )
- }
+ taxonomy: value,
+ label: selected?.name || label,
+ } );
+ } }
/>
setAttributes( { label } ) }
+ onChange={ ( value ) => setAttributes( { label: value } ) }
/>
- setAttributes( { showLabel } )
- }
+ onChange={ ( value ) => setAttributes( { showLabel: value } ) }
/>
- setAttributes( { emptyLabel } )
+ onChange={ ( value ) =>
+ setAttributes( { emptyLabel: value } )
+ }
+ />
+
+ setAttributes( { limitToCurrentResults: value } )
}
/>
-
+
{ showLabel && (
) }
);
}
+
diff --git a/src/taxonomy/index.js b/src/taxonomy/index.js
index 30d8967..20dadbc 100644
--- a/src/taxonomy/index.js
+++ b/src/taxonomy/index.js
@@ -1,11 +1,8 @@
import { registerBlockType } from '@wordpress/blocks';
-import Edit from './edit';
import metadata from './block.json';
+import Edit from './edit';
import './style-index.css';
registerBlockType( metadata.name, {
- /**
- * @see ./edit.js
- */
edit: Edit,
} );
diff --git a/src/taxonomy/render.php b/src/taxonomy/render.php
index 97258b4..e4e349e 100644
--- a/src/taxonomy/render.php
+++ b/src/taxonomy/render.php
@@ -4,27 +4,63 @@
}
$id = 'query-filter-' . wp_generate_uuid4();
-
$taxonomy = get_taxonomy( $attributes['taxonomy'] );
-if ( empty( $block->context['query']['inherit'] ) ) {
- $query_id = $block->context['queryId'] ?? 0;
- $query_var = sprintf( 'query-%d-%s', $query_id, $attributes['taxonomy'] );
- $page_var = isset( $block->context['queryId'] ) ? 'query-' . $block->context['queryId'] . '-page' : 'query-page';
- $base_url = remove_query_arg( [ $query_var, $page_var ] );
-} else {
- $query_var = sprintf( 'query-%s', $attributes['taxonomy'] );
- $page_var = 'page';
- $base_url = str_replace( '/page/' . get_query_var( 'paged' ), '', remove_query_arg( [ $query_var, $page_var ] ) );
+$target_query_id = empty( $block->context['query']['inherit'] )
+ ? (string) ( $block->context['queryId'] ?? 0 )
+ : 'main';
+
+$query_var = sprintf( 'query-%s', $attributes['taxonomy'] );
+$page_var = 'query-page';
+$base_url = remove_query_arg( [ $query_var, $page_var, 'query-post_id', 'query-id' ] );
+
+if ( ! empty( $block->context['query']['inherit'] ) ) {
+ $current_paged = (int) get_query_var( 'paged' );
+ if ( $current_paged > 1 ) {
+ $base_url = str_replace( '/page/' . $current_paged, '', $base_url );
+ }
+ $base_url = remove_query_arg( [ 'page' ], $base_url );
}
-$terms = get_terms( [
- 'hide_empty' => true,
- 'taxonomy' => $attributes['taxonomy'],
- 'number' => 100,
-] );
+$base_url = add_query_arg(
+ [
+ 'query-post_id' => $target_query_id,
+ ],
+ $base_url
+);
+$base_url = HM\Query_Loop_Filter\normalize_query_filter_url( $base_url );
+
+$terms = [];
+
+if ( ! empty( $attributes['limitToCurrentResults'] ) ) {
+ $post_ids = HM\Query_Loop_Filter\get_query_loop_post_ids_for_block( $block );
+
+ if ( ! empty( $post_ids ) ) {
+ $terms = wp_get_object_terms(
+ $post_ids,
+ $attributes['taxonomy'],
+ [
+ 'orderby' => 'name',
+ 'order' => 'ASC',
+ 'number' => 100,
+ ]
+ );
+ }
-if ( is_wp_error( $terms ) || empty( $terms ) ) {
+ if ( is_wp_error( $terms ) ) {
+ return;
+ }
+}
+
+if ( empty( $terms ) ) {
+ $terms = get_terms( [
+ 'hide_empty' => true,
+ 'taxonomy' => $attributes['taxonomy'],
+ 'number' => 100,
+ ] );
+}
+
+if ( is_wp_error( $terms ) || ( empty( $terms ) && empty( $attributes['limitToCurrentResults'] ) ) ) {
return;
}
?>
@@ -35,8 +71,20 @@
+
diff --git a/src/taxonomy/style-index.css b/src/taxonomy/style-index.css
index 621fee1..f3c507c 100644
--- a/src/taxonomy/style-index.css
+++ b/src/taxonomy/style-index.css
@@ -1,9 +1,4 @@
-@view-transition {
- navigation: auto;
-}
-
.wp-block-query-filter {
- display: flex;
- flex-direction: column;
- justify-content: stretch;
+ /* Add your editor styles here if needed. */
}
+
diff --git a/src/taxonomy/view.js b/src/taxonomy/view.js
index 2438d11..27e3e0a 100644
--- a/src/taxonomy/view.js
+++ b/src/taxonomy/view.js
@@ -35,7 +35,6 @@ const { state } = store( 'query-filter', {
value = ref.value;
}
- // Don't navigate if the search didn't really change.
if ( value === state.searchValue ) return;
state.searchValue = value;
@@ -44,3 +43,4 @@ const { state } = store( 'query-filter', {
},
},
} );
+