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. ![image](https://github.com/user-attachments/assets/e2f9b62d-91f7-4c22-87ac-078b4d031a60) 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 && ( ) } - - + $term->slug, + $page_var => false, + ], + $base_url + ); + $value = HM\Query_Loop_Filter\normalize_query_filter_url( $value ); + ?> +
+ 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', { }, }, } ); +