diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ae0b2c19..078344975 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. The format on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Each changelog entry gets prefixed with the category of the item (Added, Changed, Depreciated, Removed, Fixed, Security). +## [2026.03] +- Added: FacetWP Archive blocks & support. + ## [2026.02] - Updated: Renamed "Moose" to "ModernPress" across the project (excluding Lando configs and deployment pipelines). - Updated: Expected PHP version to v8.4, Node version to v24 LTS diff --git a/dev/templates/block/edit.js.mustache b/dev/templates/block/edit.js.mustache index 54b32d69f..77641249f 100644 --- a/dev/templates/block/edit.js.mustache +++ b/dev/templates/block/edit.js.mustache @@ -2,7 +2,8 @@ import { __ } from '@wordpress/i18n'; import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; import { PanelBody, TextControl } from '@wordpress/components'; {{#isDynamicVariant}} -import ServerSideRender from '@wordpress/server-side-render'; +import { ServerSideRender } from '@wordpress/server-side-render'; +import metadata from './block.json'; {{/isDynamicVariant}} import './editor.pcss'; @@ -16,7 +17,7 @@ export default function Edit( { attributes, setAttributes, isSelected } ) {
{{#isDynamicVariant}} {{/isDynamicVariant}} diff --git a/wp-content/plugins/core/src/Blocks/Block_Base.php b/wp-content/plugins/core/src/Blocks/Block_Base.php index ba73d1794..44911bf39 100644 --- a/wp-content/plugins/core/src/Blocks/Block_Base.php +++ b/wp-content/plugins/core/src/Blocks/Block_Base.php @@ -70,10 +70,9 @@ public function register_core_block_variations(): void { * Additionally, the selectors are prefixed with `.editor-styles-wrapper` in the editors. */ public function enqueue_core_block_public_styles(): void { - $handle = $this->get_block_style_handle(); + $block = $this->get_block_handle(); $path = $this->get_block_path(); - $args = $this->get_asset_file_args( get_theme_file_path( "dist/blocks/$path/index.asset.php" ) ); - $version = $args['version'] ?? false; + $args = $this->get_asset_file_args( get_theme_file_path( "dist/blocks/$path/editor.asset.php" ) ); $src_path = get_theme_file_path( "dist/blocks/$path/style-index.css" ); $src = get_theme_file_uri( "dist/blocks/$path/style-index.css" ); @@ -81,12 +80,13 @@ public function enqueue_core_block_public_styles(): void { return; } - if ( ! empty( $version ) ) { - $src = $src . '?ver=' . $version; - } - - wp_add_inline_style( $handle, file_get_contents( $src_path ) ); - add_editor_style( $src ); + wp_enqueue_style( + "tribe-$block", + $src, + [], + $args['version'] ?? false, + 'all' + ); } /** @@ -97,24 +97,34 @@ public function enqueue_core_block_public_styles(): void { * Additionally, the selectors are prefixed with `.editor-styles-wrapper` in the editors. */ public function enqueue_core_block_editor_styles(): void { - $path = $this->get_block_path(); - $args = $this->get_asset_file_args( get_theme_file_path( "dist/blocks/$path/editor.asset.php" ) ); - $version = $args['version'] ?? false; - $editor_src_path = get_theme_file_path( "dist/blocks/$path/editor.css" ); - $editor_src = get_theme_file_uri( "dist/blocks/$path/editor.css" ); - - if ( ! file_exists( $editor_src_path ) ) { + if ( ! is_admin() ) { return; } - if ( ! empty( $version ) ) { - $editor_src = $editor_src . '?ver=' . $version; + $block = $this->get_block_handle(); + $path = $this->get_block_path(); + $args = $this->get_asset_file_args( get_theme_file_path( "dist/blocks/$path/editor.asset.php" ) ); + $src_path = get_theme_file_path( "dist/blocks/$path/editor.css" ); + $src = get_theme_file_uri( "dist/blocks/$path/editor.css" ); + + if ( ! file_exists( $src_path ) ) { + return; } - add_editor_style( $editor_src ); + wp_enqueue_style( + "tribe-editor-$block", + $src, + [], + $args['version'] ?? false, + 'all' + ); } public function enqueue_core_block_editor_scripts(): void { + if ( ! is_admin() ) { + return; + } + $block = $this->get_block_handle(); $path = $this->get_block_path(); $args = $this->get_asset_file_args( get_theme_file_path( "dist/blocks/$path/editor.asset.php" ) ); @@ -126,7 +136,7 @@ public function enqueue_core_block_editor_scripts(): void { } wp_enqueue_script( - "admin-$block-scripts", + "tribe-editor-$block", $src, $args['dependencies'] ?? [], $args['version'] ?? false, diff --git a/wp-content/plugins/core/src/Blocks/Blocks_Definer.php b/wp-content/plugins/core/src/Blocks/Blocks_Definer.php index c64adcfd4..dfbb555eb 100644 --- a/wp-content/plugins/core/src/Blocks/Blocks_Definer.php +++ b/wp-content/plugins/core/src/Blocks/Blocks_Definer.php @@ -48,6 +48,9 @@ public function define(): array { 'tribe/carousel', 'tribe/carousel-slide', 'tribe/copyright', + 'tribe/facetwp-archive', + 'tribe/facetwp-filter-bar', + 'tribe/facetwp-grid', 'tribe/horizontal-tab', 'tribe/horizontal-tabs', 'tribe/icon-card', diff --git a/wp-content/plugins/core/src/Blocks/Blocks_Subscriber.php b/wp-content/plugins/core/src/Blocks/Blocks_Subscriber.php index d69163139..a9ab8247e 100644 --- a/wp-content/plugins/core/src/Blocks/Blocks_Subscriber.php +++ b/wp-content/plugins/core/src/Blocks/Blocks_Subscriber.php @@ -49,26 +49,18 @@ public function register(): void { } }, 10, 0 ); - /** - * Enqueue styles in the editor for WP Core blocks - */ - add_action( 'admin_init', function (): void { - foreach ( $this->container->get( Blocks_Definer::EXTENDED ) as $block ) { - // core block public styles - $block->enqueue_core_block_public_styles(); - // core block editor-only styles - $block->enqueue_core_block_editor_styles(); - } - }, 10, 0 ); - /** * Enqueue block editor-only scripts * * Core blocks shouldn't ever have FE scripts and should only include * editor scripts in order to override default block functionality */ - add_action( 'enqueue_block_editor_assets', function (): void { + add_action( 'enqueue_block_assets', function (): void { foreach ( $this->container->get( Blocks_Definer::EXTENDED ) as $block ) { + // core block public styles + $block->enqueue_core_block_public_styles(); + // core block editor-only styles + $block->enqueue_core_block_editor_styles(); // core block editor-only scripts $block->enqueue_core_block_editor_scripts(); } diff --git a/wp-content/plugins/core/src/Components/Abstracts/Abstract_Block_Controller.php b/wp-content/plugins/core/src/Components/Abstracts/Abstract_Block_Controller.php index ac10543b5..1b5385c19 100644 --- a/wp-content/plugins/core/src/Components/Abstracts/Abstract_Block_Controller.php +++ b/wp-content/plugins/core/src/Components/Abstracts/Abstract_Block_Controller.php @@ -10,6 +10,12 @@ abstract class Abstract_Block_Controller extends Abstract_Controller { * @var array */ protected array $attributes; + + /** + * @var array + */ + protected array $context; + protected string $block_classes; protected string $block_styles; private Block_Animation_Attributes|false $block_animation_attributes; @@ -18,6 +24,7 @@ abstract class Abstract_Block_Controller extends Abstract_Controller { public function __construct( array $args = [] ) { $this->attributes = $args['attributes'] ?? []; + $this->context = $args['context'] ?? []; $this->block_classes = $args['block_classes'] ?? ''; $this->block_styles = $args['block_styles'] ?? ''; $this->block_animation_attributes = $this->attributes ? new Block_Animation_Attributes( $this->attributes ) : false; diff --git a/wp-content/plugins/core/src/Components/Blocks/FacetWP_Archive_Controller.php b/wp-content/plugins/core/src/Components/Blocks/FacetWP_Archive_Controller.php new file mode 100644 index 000000000..469956429 --- /dev/null +++ b/wp-content/plugins/core/src/Components/Blocks/FacetWP_Archive_Controller.php @@ -0,0 +1,19 @@ +filter_bar_position = $this->attributes['filterBarPosition'] ?? 'top'; + + $this->block_classes .= " b-facetwp-archive--filter-bar-{$this->filter_bar_position}"; + } + +} diff --git a/wp-content/plugins/core/src/Components/Blocks/FacetWP_Filter_Bar_Controller.php b/wp-content/plugins/core/src/Components/Blocks/FacetWP_Filter_Bar_Controller.php new file mode 100644 index 000000000..9c1a4766f --- /dev/null +++ b/wp-content/plugins/core/src/Components/Blocks/FacetWP_Filter_Bar_Controller.php @@ -0,0 +1,154 @@ + + */ + protected array $facets; + protected string $filter_bar_position; + + /** + * Facet types that are not wrapped in accordions when filter bar position is sidebar. + * + * @var array + */ + protected array $accordion_excluded_types = [ 'search', 'reset' ]; + + /** + * Facet types that should not have a label displayed. + * + * @var array + */ + protected array $no_label_types = [ 'reset' ]; + + public function __construct( array $args = [] ) { + parent::__construct( $args ); + + $this->facets = $this->attributes['facets'] ?? []; + $this->filter_bar_position = $this->context['tribe/facetwp-archive/filterBarPosition'] ?? 'top'; + } + + /** + * Get the facets array with display labels. + */ + public function get_facets(): array { + return array_map( static function ( array $facet ): array { + $facet['display_label'] = $facet['displayLabel'] ?? $facet['label']; + + return $facet; + }, $this->facets ); + } + + /** + * Get the filter bar position. + */ + public function get_filter_bar_position(): string { + return $this->filter_bar_position; + } + + /** + * Whether this facet should have a label displayed. + * + * @param array $facet + */ + public function should_hide_facet_label( array $facet ): bool { + $type = strtolower( $facet['type'] ?? '' ); + + return in_array( $type, $this->no_label_types, true ); + } + + /** + * Whether this facet should be wrapped in a details/summary accordion (sidebar, excluding $accordion_excluded_types). + * + * @param array $facet + */ + public function should_wrap_facet_in_accordion( array $facet ): bool { + if ( $this->get_filter_bar_position() !== 'sidebar' ) { + return false; + } + + $type = strtolower( $facet['type'] ?? '' ); + + return ! in_array( $type, $this->accordion_excluded_types, true ); + } + + /** + * Grid slot for top filter bar layout: facet-1, facet-2, facet-3, search, or reset. + * Based on facet type and order in the facets list (not DOM order). + * + * @param array $current_facet + */ + public function get_grid_slot( array $current_facet ): ?string { + $current_facet_type = strtolower( $current_facet['type'] ?? '' ); + + if ( $current_facet_type === 'search' ) { + return 'search'; + } + + if ( $current_facet_type === 'reset' ) { + return 'reset'; + } + + $content_index = 0; + foreach ( $this->get_facets() as $facet ) { + $facet_type = strtolower( $facet['type'] ?? '' ); + + /** + * The "top" layout doesn't wrap these facet types in an accordion, but + * we can use the same types to determine if the current facet should + * receive a grid slot. + */ + if ( in_array( $facet_type, $this->accordion_excluded_types, true ) ) { + continue; + } + + if ( ( $current_facet['slug'] ?? '' ) === ( $facet['slug'] ?? '' ) ) { + /** + * We are limiting the grid to 3 facets (that aren't the "search" + * or "reset" facet types) for the "top" layout. + */ + if ( $content_index >= 3 ) { + return null; + } + + return 'facet-' . ( $content_index + 1 ); + } + $content_index++; + } + + return null; + } + + /** + * HTML attributes string for a facet wrapper (class and optional data-grid-slot). + * + * @param array $facet + */ + public function get_facet_wrapper_attributes( array $facet ): string { + $classes = [ 'b-facetwp-filter-bar__facet' ]; + + if ( $this->should_wrap_facet_in_accordion( $facet ) ) { + $classes[] = 'b-facetwp-filter-bar__facet--accordion'; + } + + $attrs = [ 'class' => implode( ' ', $classes ) ]; + + $grid_slot = $this->get_grid_slot( $facet ); + + if ( $grid_slot !== null ) { + $attrs['data-grid-slot'] = $grid_slot; + } + + return implode( ' ', array_map( + static fn ( string $key, string $value ): string => $key . '="' . esc_attr( $value ) . '"', + array_keys( $attrs ), + $attrs + ) ); + } + +} diff --git a/wp-content/plugins/core/src/Components/Blocks/FacetWP_Grid_Controller.php b/wp-content/plugins/core/src/Components/Blocks/FacetWP_Grid_Controller.php new file mode 100644 index 000000000..61a7dcb50 --- /dev/null +++ b/wp-content/plugins/core/src/Components/Blocks/FacetWP_Grid_Controller.php @@ -0,0 +1,62 @@ +block_classes .= ' facetwp-template'; + + $this->post_type = $this->attributes['postType'] ?? 'post'; + $this->posts_per_page = absint( $this->attributes['postsPerPage'] ?? 12 ); + $this->show_pagination = $this->attributes['showPagination'] ?? false; + + $this->set_query(); + } + + public function get_query(): \WP_Query { + return $this->query; + } + + public function get_pagination_facet(): ?array { + if ( ! function_exists( 'FWP' ) ) { + return null; + } + + $facets = FWP()->helper->get_facets(); + + $pagination_facet = array_values( array_filter( $facets, static function ( array $facet ): bool { + return $facet['type'] === 'pager' && $facet['pager_type'] === 'numbers'; + } ) ); + + return $pagination_facet[0] ?? null; + } + + public function should_show_pagination(): bool { + // We need to make sure a pagination facet exists + if ( is_null( $this->get_pagination_facet() ) ) { + return false; + } + + return $this->show_pagination && $this->query->max_num_pages > 1; + } + + private function set_query(): void { + $this->query = new \WP_Query( [ + 'post_type' => $this->post_type, + 'post_status' => 'publish', + 'posts_per_page' => $this->posts_per_page, + 'facetwp' => true, + ] ); + } + +} diff --git a/wp-content/plugins/core/src/Integrations/FacetWP.php b/wp-content/plugins/core/src/Integrations/FacetWP.php new file mode 100644 index 000000000..a724600d5 --- /dev/null +++ b/wp-content/plugins/core/src/Integrations/FacetWP.php @@ -0,0 +1,108 @@ +helper->get_facets(); + } + + /** + * Add an id attribute to the facet's first form control so label[for] works for accessibility. + * + * @param string $output Facet HTML. + * @param array $params Facet params; $params['facet']['name'] is the facet slug. + * + * @return string Modified HTML. + */ + public function add_facet_control_id( string $output, array $params ): string { + $name = $params['facet']['name'] ?? ''; + if ( $name === '' ) { + return $output; + } + + $id = 'facet-' . $name; + + // Add id to the first so label[for] targets the focusable control. + $output = preg_replace( + '/<(select|input)(\s)/', + '<$1 id="' . esc_attr( $id ) . '"$2', + $output, + 1 + ); + + return $output; + } + + public function remove_facetwp_counts( string $output, array $params ): string { + $remove_from_types = [ 'checkboxes', 'radio', 'hierarchy', 'range_list', 'time_since' ]; + + if ( ! in_array( $params['facet']['type'], $remove_from_types, true ) ) { + return $output; + } + + return preg_replace( '/[^<]*<\/span>/', '', $output ); + } + + public function register_custom_facets( array $facets ): array { + // Pagination facet + $facets[] = [ + 'name' => 'pagination', + 'label' => 'Pagination', + 'type' => 'pager', + 'pager_type' => 'numbers', + 'inner_size' => '1', + 'dots_label' => '…', + 'prev_label' => 'Previous page', + 'next_label' => 'Next page', + 'count_text_plural' => '[lower] - [upper] of [total] results', + 'count_text_singular' => '1 result', + 'count_text_none' => 'No results', + 'scroll_target' => '.facetwp-template', + 'scroll_offset' => '-150', + 'load_more_text' => 'Load more', + 'loading_text' => 'Loading...', + 'default_label' => 'Per page', + 'per_page_options' => '10, 25, 50, 100', + ]; + + // Search facet + $facets[] = [ + 'enable_relevance' => 'yes', + 'name' => 'search', + 'label' => 'Search', + 'type' => 'search', + 'search_engine' => '', + 'placeholder' => 'Enter keyword', + 'auto_refresh' => 'yes', + ]; + + // Reset facet + $facets[] = [ + 'name' => 'reset_filters', + 'label' => 'Reset Filters', + 'type' => 'reset', + 'reset_facets' => [], + 'reset_ui' => 'button', + 'reset_text' => 'Clear all', + 'reset_mode' => 'off', + 'auto_hide' => 'no', + ]; + + return $facets; + } + + public function rewrite_pagination_link_tags( string $html, array $params ): string { + if ( '' === $params['page'] ) { + $html = str_replace( [ '' ], [ '' ], $html ); + } + + return str_replace( [ '' ], [ ' + + + + + + { isModalOpen && ( + + + + + + + + ) } +
+ ); +}; + +function Edit( { + attributes, + setAttributes, + isSelected, + context, + facetwpFacets, +} ) { + const blockProps = useBlockProps(); + const [ selectedFacet, setSelectedFacet ] = useState( + facetwpFacets[ 0 ]?.name ?? '' + ); + + const { facets } = attributes; + + const filterBarPosition = + context[ 'tribe/facetwp-archive/filterBarPosition' ]; + + const handleAddFacet = () => { + const mappedFacet = facetwpFacets.find( + ( facet ) => facet.name === selectedFacet + ); + + if ( + ! mappedFacet || + facets.some( ( f ) => f.slug === mappedFacet.name ) + ) { + return; + } + + setAttributes( { + facets: [ + ...facets, + { + slug: mappedFacet.name, + label: mappedFacet.label, + type: mappedFacet.type ?? '', + displayLabel: mappedFacet.label, + }, + ], + } ); + }; + + const handleRemoveFacet = ( slug ) => { + setAttributes( { + facets: facets.filter( ( facet ) => facet.slug !== slug ), + } ); + }; + + const handleDragEnd = useCallback( + ( event ) => { + const { active, over } = event; + if ( over && active.id !== over.id ) { + const oldIndex = facets.findIndex( + ( f ) => f.slug === active.id + ); + const newIndex = facets.findIndex( + ( f ) => f.slug === over.id + ); + if ( oldIndex !== -1 && newIndex !== -1 ) { + setAttributes( { + facets: arrayMove( facets, oldIndex, newIndex ), + } ); + } + } + }, + [ facets, setAttributes ] + ); + + const handleDisplayLabelChange = useCallback( + ( slug, value ) => { + setAttributes( { + facets: facets.map( ( f ) => + f.slug === slug + ? { + ...f, + displayLabel: + value && value.trim() !== '' + ? value.trim() + : undefined, + } + : f + ), + } ); + }, + [ facets, setAttributes ] + ); + + const sensors = useSensors( useSensor( PointerSensor ) ); + + return ( +
+ + { isSelected && ( + + +
+ { filterBarPosition === 'top' && ( +

+ { __( + 'The filter bar will be displayed at the top of the page. Typically facets of type "dropdown" or "fselect" are best suited for this position.', + 'tribe' + ) } +

+ ) } + { filterBarPosition === 'sidebar' && ( +

+ { __( + 'The filter bar will be displayed in the sidebar. Typically facets of type "checkbox" or "radio" are best suited for this position.', + 'tribe' + ) } +

+ ) } +

+ { __( + 'To change the position of the filter bar, use the "Filter Bar Position" setting in the parent FacetWP Archive block.', + 'tribe' + ) } +

+
+ + + f.slug ) } + > + { facets.map( ( facet ) => ( + + ) ) } + + + + + ( { + label: `${ facet.label } (${ facet.type })`, + value: facet.name, + disabled: facets.some( + ( f ) => f.slug === facet.name + ), + } ) ) } + onChange={ ( value ) => + setSelectedFacet( value ) + } + /> + + +
+
+ ) } +
+ ); +} + +export default withSelect( ( select, ownProps ) => { + return { + facetwpFacets: + select( 'core/editor' ).getEditorSettings()?.facetwpFacets ?? [], + ...ownProps, + }; +} )( Edit ); diff --git a/wp-content/themes/core/blocks/tribe/facetwp-filter-bar/editor.pcss b/wp-content/themes/core/blocks/tribe/facetwp-filter-bar/editor.pcss new file mode 100644 index 000000000..4451a708a --- /dev/null +++ b/wp-content/themes/core/blocks/tribe/facetwp-filter-bar/editor.pcss @@ -0,0 +1,28 @@ +/* ------------------------------------------------------------------------- + * + * Block - FacetWP Filter Bar - Styles - Editor Only + * + * ------------------------------------------------------------------------- */ + +.b-facetwp-filter-bar__icon-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 2px; + margin: 0; + border: none; + background: none; + color: inherit; + cursor: pointer; + border-radius: 2px; +} + +.b-facetwp-filter-bar__icon-btn:hover { + background: var(--wp-admin-theme-color, #007cba); + color: #fff; +} + +.b-facetwp-filter-bar__icon-btn--remove:hover { + background: #d63638; + color: #fff; +} diff --git a/wp-content/themes/core/blocks/tribe/facetwp-filter-bar/index.js b/wp-content/themes/core/blocks/tribe/facetwp-filter-bar/index.js new file mode 100644 index 000000000..a3596f2f0 --- /dev/null +++ b/wp-content/themes/core/blocks/tribe/facetwp-filter-bar/index.js @@ -0,0 +1,13 @@ +import { registerBlockType } from '@wordpress/blocks'; + +import './style.pcss'; + +import Edit from './edit'; +import metadata from './block.json'; + +registerBlockType( metadata.name, { + /** + * @see ./edit.js + */ + edit: Edit, +} ); diff --git a/wp-content/themes/core/blocks/tribe/facetwp-filter-bar/js/dropdown-actions.js b/wp-content/themes/core/blocks/tribe/facetwp-filter-bar/js/dropdown-actions.js new file mode 100644 index 000000000..7351aa3b4 --- /dev/null +++ b/wp-content/themes/core/blocks/tribe/facetwp-filter-bar/js/dropdown-actions.js @@ -0,0 +1,225 @@ +/** + * fSelect dropdown actions (top filter bar layout). + * + * Injects "Clear filter" / "Filter" buttons into FacetWP fSelect dropdowns + * and handles keyboard navigation within the action toolbar. + * + * @see https://facetwp.com/help-center/developers/javascript-reference/ + */ + +const el = {}; + +/** + * @function facetDropdownActions + * + * @description Returns the HTML markup for the facet dropdown action bar (Clear filter + Filter buttons). + * + * @param {string} facet Facet name/slug. + * @return {string} HTML string for the actions toolbar. + */ +const facetDropdownActions = ( + facet +) => ``; + +/** + * @function handleCloseFacet + * + * @description Closes the fSelect dropdown via FacetWP's fUtil. + * + * @param {Event} e Click event. + */ +const handleCloseFacet = ( e ) => { + const facet = e.currentTarget.closest( '.facetwp-facet' ); + const fselectEl = facet.querySelector( '.facetwp-dropdown' ); + window.fUtil( fselectEl ).nodes[ 0 ].fselect.close(); +}; + +/** + * @function handleClearFacet + * + * @description Clears selections for a single facet via FWP.reset( facetName ). + * + * @param {Event} e Click event. + */ +const handleClearFacet = ( e ) => { + const facetName = e.currentTarget.getAttribute( 'data-facet' ); + window.FWP.reset( facetName ); +}; + +/** + * @function handleActionsKeydown + * + * @description Handles keyboard support for the dropdown action bar (toolbar). + * + * @param {KeyboardEvent} e Keyboard event. + */ +const handleActionsKeydown = ( e ) => { + const toolbar = e.currentTarget; + const buttons = [ ...toolbar.querySelectorAll( 'button' ) ]; + const currentIndex = buttons.indexOf( e.target ); + + if ( currentIndex === -1 ) { + return; + } + + switch ( e.key ) { + case 'ArrowUp': { + if ( currentIndex === 0 ) { + const facet = toolbar.closest( '.facetwp-facet' ); + const optionsContainer = facet?.querySelector( '.fs-options' ); + const options = optionsContainer + ? [ ...optionsContainer.querySelectorAll( '.fs-option' ) ] + : []; + const lastOption = options[ options.length - 1 ]; + if ( lastOption ) { + e.preventDefault(); + e.stopPropagation(); + lastOption.focus(); + } + } else { + e.preventDefault(); + e.stopPropagation(); + buttons[ currentIndex - 1 ].focus(); + } + break; + } + + case 'ArrowLeft': + case 'ArrowRight': { + e.preventDefault(); + e.stopPropagation(); + const direction = e.key === 'ArrowRight' ? 1 : -1; + const nextIndex = + ( currentIndex + direction + buttons.length ) % buttons.length; + buttons[ nextIndex ].focus(); + break; + } + + case 'Enter': + case ' ': { + e.stopPropagation(); + break; + } + + default: + break; + } +}; + +/** + * @function handleDropdownKeydown + * + * @description Handles Arrow Down on the last fs-option moves focus to the first action button. + * + * @param {KeyboardEvent} e Keyboard event. + */ +const handleDropdownKeydown = ( e ) => { + if ( e.key !== 'ArrowDown' ) { + return; + } + + const facet = e.currentTarget.closest( '.facetwp-facet' ); + const optionsContainer = facet?.querySelector( '.fs-options' ); + const actionsBar = facet?.querySelector( '.fs-actions' ); + + if ( ! optionsContainer || ! actionsBar ) { + return; + } + + const options = [ ...optionsContainer.querySelectorAll( '.fs-option' ) ]; + const actionButtons = [ ...actionsBar.querySelectorAll( 'button' ) ]; + const activeEl = facet.ownerDocument.activeElement; + const lastOption = options[ options.length - 1 ]; + + if ( activeEl === lastOption && actionButtons[ 0 ] ) { + e.preventDefault(); + e.stopPropagation(); + actionButtons[ 0 ].focus(); + } +}; + +/** + * @function bindEvents + * + * @description Binds events to each fSelect facet's clear/close and keyboard handlers. + */ +const bindEvents = () => { + if ( ! el.facets ) { + return; + } + el.facets.forEach( ( facet ) => { + const clearFacet = facet.querySelector( + '[data-js="facet-clear-filter"]' + ); + if ( clearFacet ) { + clearFacet.addEventListener( 'click', handleClearFacet ); + } + + const closeFacet = facet.querySelector( '[data-js="facet-close"]' ); + if ( closeFacet ) { + closeFacet.addEventListener( 'click', handleCloseFacet ); + } + + const actionsBar = facet.querySelector( '.fs-actions' ); + if ( actionsBar ) { + actionsBar.addEventListener( 'keydown', handleActionsKeydown ); + } + + const dropdown = facet.querySelector( '.fs-dropdown' ); + if ( dropdown ) { + dropdown.addEventListener( 'keydown', handleDropdownKeydown ); + } + } ); +}; + +/** + * @function createDropdownButtons + * + * @description Injects the action bar markup into each fSelect dropdown. + */ +const createDropdownButtons = () => { + if ( ! el.facets ) { + return; + } + el.facets.forEach( ( facet ) => { + const facetName = facet.getAttribute( 'data-name' ); + const dropdownActionsEl = facetDropdownActions( facetName ); + const dropdown = facet.querySelector( '.fs-dropdown' ); + + if ( dropdown ) { + dropdown.innerHTML += dropdownActionsEl; + } + } ); +}; + +/** + * @function cacheElements + * + * @description Caches filter bar and fSelect facet elements for dropdown actions. + */ +const cacheElements = () => { + el.filters = document.querySelector( '.b-facetwp-filter-bar' ); + el.facets = document.querySelectorAll( + '.facetwp-facet.facetwp-type-fselect' + ); +}; + +/** + * @function initDropdownActions + * + * @description Initializes fSelect dropdown actions: caches elements, injects buttons, binds events. + * Call this on the facetwp-loaded event. + */ +export const initDropdownActions = () => { + cacheElements(); + + const actions = el.filters?.querySelector( '.fs-actions' ); + + if ( ! actions && el.filters && el.facets?.length ) { + createDropdownButtons(); + bindEvents(); + } +}; diff --git a/wp-content/themes/core/blocks/tribe/facetwp-filter-bar/js/flyout.js b/wp-content/themes/core/blocks/tribe/facetwp-filter-bar/js/flyout.js new file mode 100644 index 000000000..de6fea1b4 --- /dev/null +++ b/wp-content/themes/core/blocks/tribe/facetwp-filter-bar/js/flyout.js @@ -0,0 +1,275 @@ +/** + * Mobile filter flyout (sidebar layout only). + * + * Handles the "Search & Refine" trigger bar, full-screen flyout dialog, + * focus trap, close/show-results, and "Clear all" visibility. + */ + +import { bodyLock } from 'utils/tools'; + +const FOCUSABLE_SELECTOR = + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; + +const SELECTORS = { + block: '.b-facetwp-filter-bar', + grid: '.b-facetwp-filter-bar__grid', + flyout: '[data-js="facetwp-filter-flyout"]', + trigger: '[data-js="facetwp-filter-open"]', + closeBtn: '[data-js="facetwp-filter-close"]', + showResultsBtn: '[data-js="facetwp-filter-show-results"]', + clearWrap: '[data-js="facetwp-filter-clear-wrap"]', + clearAllBtn: '[data-js="facetwp-filter-clear-all"]', + activeFacet: '[aria-checked="true"], .fs-option.selected', + searchInput: + '.facetwp-type-search input[type="text"], .facetwp-type-search input[type="search"]', +}; + +/** + * @function getFocusables + * + * @description Gets focusable elements within a root. + * + * @param {Element} root Container to search for focusable elements. + * @return {Element[]} Focusable elements within root. + */ +export const getFocusables = ( root ) => + [ ...root.querySelectorAll( FOCUSABLE_SELECTOR ) ].filter( + ( node ) => + ! node.hasAttribute( 'disabled' ) && node.offsetParent !== null + ); + +/** + * @function trapFocus + * + * @description Traps focus within dialog on Tab and closes on Escape. + * + * @param {KeyboardEvent} e Keyboard event. + * @param {Element} dialog Dialog element to trap focus within. + */ +const trapFocus = ( e, dialog ) => { + if ( e.key !== 'Tab' ) { + return; + } + + const focusables = getFocusables( dialog ); + + if ( focusables.length === 0 ) { + return; + } + + const first = focusables[ 0 ]; + const last = focusables[ focusables.length - 1 ]; + const doc = dialog.ownerDocument; + const active = doc.activeElement; + + if ( e.shiftKey && active === first ) { + e.preventDefault(); + last.focus(); + } else if ( ! e.shiftKey && active === last ) { + e.preventDefault(); + first.focus(); + } +}; + +/** + * @function openFlyout + * + * @description Opens the filter flyout for a sidebar block. + * + * @param {Element} block Filter bar block element. + */ +const openFlyout = ( block ) => { + const flyout = block.querySelector( SELECTORS.flyout ); + const trigger = block.querySelector( SELECTORS.trigger ); + const closeBtn = block.querySelector( SELECTORS.closeBtn ); + + if ( ! flyout || ! trigger || ! closeBtn ) { + return; + } + + trigger.setAttribute( 'aria-expanded', 'true' ); + flyout.setAttribute( 'aria-hidden', 'false' ); + flyout.classList.add( 'is-style-white' ); + flyout.classList.add( 'is-open' ); + bodyLock( true ); + closeBtn.focus(); + flyout.addEventListener( 'keydown', flyoutKeydown ); + document.addEventListener( 'keydown', handleDocumentKeydown ); +}; + +/** + * @function closeFlyout + * + * @description Closes the filter flyout for a sidebar block. + * + * @param {Element} block Filter bar block element. + */ +const closeFlyout = ( block ) => { + const flyout = block.querySelector( SELECTORS.flyout ); + const trigger = block.querySelector( SELECTORS.trigger ); + + if ( ! flyout || ! trigger ) { + return; + } + + trigger.setAttribute( 'aria-expanded', 'false' ); + flyout.setAttribute( 'aria-hidden', 'true' ); + flyout.classList.remove( 'is-open' ); + flyout.classList.remove( 'is-style-white' ); + bodyLock( false ); + trigger.focus(); + flyout.removeEventListener( 'keydown', flyoutKeydown ); + document.removeEventListener( 'keydown', handleDocumentKeydown ); +}; + +/** + * Document-level keydown: closes open flyout on Escape so it works even when + * focus is still on the trigger (outside the flyout). + * + * @param {KeyboardEvent} e Keyboard event. + */ +const handleDocumentKeydown = ( e ) => { + if ( e.key !== 'Escape' ) { + return; + } + + const openFlyoutEl = document.querySelector( + `${ SELECTORS.flyout }.is-open` + ); + + if ( ! openFlyoutEl ) { + return; + } + + const block = openFlyoutEl.closest( SELECTORS.block ); + + if ( block ) { + e.preventDefault(); + closeFlyout( block ); + } +}; + +/** + * @function flyoutKeydown + * + * @description Keydown handler for the flyout (Escape + focus trap). + * + * @param {KeyboardEvent} e Keyboard event. + */ +const flyoutKeydown = ( e ) => { + if ( e.key === 'Escape' ) { + const flyout = e.currentTarget; + const block = flyout.closest( SELECTORS.block ); + + if ( block ) { + e.preventDefault(); + closeFlyout( block ); + } + + return; + } + + trapFocus( e, e.currentTarget ); +}; + +/** + * @function hasActiveFilters + * + * @description Whether the block has any active facet selections. + * + * @param {Element} block Filter bar block element. + * @return {boolean} Whether any facet has an active selection. + */ +export const hasActiveFilters = ( block ) => { + const grid = block.querySelector( SELECTORS.grid ); + + if ( ! grid ) { + return false; + } + + const checked = grid.querySelectorAll( SELECTORS.activeFacet ); + const searchInput = grid.querySelector( SELECTORS.searchInput ); + const hasSearchValue = + searchInput && searchInput.value && searchInput.value.trim() !== ''; + return checked.length > 0 || hasSearchValue; +}; + +/** + * @function updateClearAllVisibility + * + * @description Shows or hides the "Clear all" wrap based on active filters. + * + * @param {Element} block Filter bar block element. + */ +export const updateClearAllVisibility = ( block ) => { + const wrap = block.querySelector( SELECTORS.clearWrap ); + + if ( ! wrap ) { + return; + } + + if ( ! hasActiveFilters( block ) ) { + wrap.setAttribute( 'hidden', '' ); + + return; + } + + wrap.removeAttribute( 'hidden' ); +}; + +/** + * @function onFacetWPUpdate + * + * @description Updates the clear all visibility on FacetWP update. + * + * @param {Element} block Filter bar block element. + */ +const onFacetWPUpdate = ( block ) => updateClearAllVisibility( block ); + +/** + * @function bindFlyoutEvents + * + * @description Binds open/close/clear events for the mobile flyout on a sidebar block. + * + * @param {Element} block Filter bar block element. + */ +export const bindFlyoutEvents = ( block ) => { + const trigger = block.querySelector( SELECTORS.trigger ); + + if ( trigger ) { + trigger.addEventListener( 'click', () => openFlyout( block ) ); + } + + const closeBtn = block.querySelector( SELECTORS.closeBtn ); + + if ( closeBtn ) { + closeBtn.addEventListener( 'click', () => closeFlyout( block ) ); + } + + const showResultsBtn = block.querySelector( SELECTORS.showResultsBtn ); + + if ( showResultsBtn ) { + showResultsBtn.addEventListener( 'click', () => closeFlyout( block ) ); + } + + const clearAllBtn = block.querySelector( SELECTORS.clearAllBtn ); + + if ( clearAllBtn ) { + clearAllBtn.addEventListener( 'click', () => { + if ( typeof window.FWP !== 'undefined' ) { + window.FWP.reset(); + } + closeFlyout( block ); + } ); + } + + document.addEventListener( 'facetwp-loaded', () => + onFacetWPUpdate( block ) + ); + + document.addEventListener( 'facetwp-refresh', () => + onFacetWPUpdate( block ) + ); + + onFacetWPUpdate( block ); +}; diff --git a/wp-content/themes/core/blocks/tribe/facetwp-filter-bar/render.php b/wp-content/themes/core/blocks/tribe/facetwp-filter-bar/render.php new file mode 100644 index 000000000..6f0e24709 --- /dev/null +++ b/wp-content/themes/core/blocks/tribe/facetwp-filter-bar/render.php @@ -0,0 +1,124 @@ + $attributes, + 'context' => $block->context, + 'block_classes' => 'b-facetwp-filter-bar', +] ); + +$is_sidebar = $c->get_filter_bar_position() === 'sidebar'; +$wrapper_attrs = [ + 'class' => esc_attr( $c->get_block_classes() ), + 'style' => $c->get_block_styles(), +]; + +if ( $is_sidebar ) { + $flyout_id = 'facetwp-filter-flyout-' . wp_unique_id(); + $flyout_title_id = 'facetwp-filter-flyout-title-' . wp_unique_id(); + + $wrapper_attrs['data-filter-bar-position'] = 'sidebar'; +} +?> +
> + +
+ + +
+ + +
+ get_facets() as $facet ) : ?> + should_wrap_facet_in_accordion( $facet ) ) : ?> +
get_facet_wrapper_attributes( $facet ); ?>> + +
+ + + +
+
+ +
get_facet_wrapper_attributes( $facet ); ?>> + should_hide_facet_label( $facet ) ) : ?> + + + + + +
+ + +
+ +
diff --git a/wp-content/themes/core/blocks/tribe/facetwp-filter-bar/style.pcss b/wp-content/themes/core/blocks/tribe/facetwp-filter-bar/style.pcss new file mode 100644 index 000000000..1df449ee4 --- /dev/null +++ b/wp-content/themes/core/blocks/tribe/facetwp-filter-bar/style.pcss @@ -0,0 +1,401 @@ +/* ------------------------------------------------------------------------- + * + * Block - FacetWP Filter Bar - Styles - FE / Editor + * + * ------------------------------------------------------------------------- */ + +.b-facetwp-filter-bar { + container: facetwp-filter-bar / inline-size; +} + +/* ------------------------------------------------------------------------- + * FacetWP Filter Bar - Grid + * ------------------------------------------------------------------------- */ + +.b-facetwp-filter-bar__grid { + + .b-facetwp-archive--filter-bar-top & { + display: flex; + flex-direction: column; + gap: var(--spacer-20); + + @container facetwp-filter-bar (min-width: 992px) { + display: grid; + grid-template-areas: + "facet-1 facet-2 facet-3 search" + "reset reset reset reset"; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: var(--spacer-30) var(--grid-gutter); + + /* Grid placement by identity (data-grid-slot from PHP), not DOM order */ + .b-facetwp-filter-bar__facet[data-grid-slot="facet-1"] { + grid-area: facet-1; + } + + .b-facetwp-filter-bar__facet[data-grid-slot="facet-2"] { + grid-area: facet-2; + } + + .b-facetwp-filter-bar__facet[data-grid-slot="facet-3"] { + grid-area: facet-3; + } + + .b-facetwp-filter-bar__facet[data-grid-slot="search"] { + grid-area: search; + } + + .b-facetwp-filter-bar__facet[data-grid-slot="reset"] { + grid-area: reset; + } + } + } +} + + /* ------------------------------------------------------------------------- + * FacetWP Filter Bar - Sidebar - Facets + * ------------------------------------------------------------------------- */ + +.b-facetwp-archive--filter-bar-sidebar .b-facetwp-filter-bar__facet { + padding: var(--spacer-40) var(--grid-margin); + border-top: 1px solid var(--color-border-secondary); + + &:first-child { + border-top: 0; + } + + @container facetwp-archive (min-width: 992px) { + padding: var(--spacer-40) 0; + + &:first-child { + padding-top: 0; + border-top: 0; + } + } +} + +/* ------------------------------------------------------------------------- + * FacetWP Filter Bar - Facet - Label + * ------------------------------------------------------------------------- */ + +.b-facetwp-filter-bar__facet-label { + + @mixin t-body; + display: block; + margin-bottom: var(--spacer-10); + font-weight: var(--font-weight-bold) +} + +/* ------------------------------------------------------------------------- + * FacetWP Filter Bar - Accordion - Summary + * ------------------------------------------------------------------------- */ + +.b-facetwp-filter-bar__facet-summary { + + @mixin t-transparent-underline; + @mixin t-display-xx-small; + --accordion-icon-size: 16px; + width: 100%; + position: relative; + padding: 0 calc(var(--spacer-20) + var(--accordion-icon-size)) 0 0; + margin: 0; + list-style: none; + cursor: pointer; + user-select: none; + + @media (--has-hover) { + + &:hover { + + @mixin t-transparent-underline-hover; + } + } + + &:focus:not(:focus-visible) { + + @mixin t-transparent-underline-hover; + } + + &:focus-visible { + + @mixin focus-visible; + } +} + +/* ------------------------------------------------------------------------- + * FacetWP Filter Bar - Accordion - Summary - Chevron + * ------------------------------------------------------------------------- */ + +.b-facetwp-filter-bar__facet-summary::-webkit-details-marker, +.b-facetwp-filter-bar__facet-summary::marker { + display: none; +} + +.b-facetwp-filter-bar__facet-summary::after { + content: ""; + display: block; + width: var(--accordion-icon-size); + height: var(--accordion-icon-size); + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%) rotate(90deg) !important; /* override WP default rotation */ + mask: var(--icon-chevron-right) center no-repeat; + mask-size: contain; + background-color: currentcolor; + transition: var(--transition); +} + +.b-facetwp-filter-bar__facet--accordion[open] .b-facetwp-filter-bar__facet-summary::after { + transform: translateY(-50%) rotate(-90deg) !important; +} + +/* ------------------------------------------------------------------------- + * FacetWP Filter Bar - Accordion - Content + * ------------------------------------------------------------------------- */ + +.b-facetwp-filter-bar__facet-content { + padding-top: var(--spacer-40); +} + +/* ------------------------------------------------------------------------- + * FacetWP Filter Bar - Sidebar Mobile - Trigger Bar + Flyout + * ------------------------------------------------------------------------- */ + +/* Mobile trigger bar (sidebar layout only, visible below desktop breakpoint) */ +.b-facetwp-filter-bar[data-filter-bar-position="sidebar"] .b-facetwp-filter-bar__mobile-trigger { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--spacer-20); + + @container facetwp-archive (min-width: 992px) { + display: none; + } +} + +/* ------------------------------------------------------------------------- + * FacetWP Filter Bar - Sidebar Mobile - Trigger Button + * ------------------------------------------------------------------------- */ + +.b-facetwp-filter-bar__trigger-btn { + --trigger-icon-size: 24px; + display: inline-flex; + align-items: center; + gap: var(--spacer-10); + padding: 0; + border: 0; + background: none; + cursor: pointer; + color: var(--color-text); + + @media (--has-hover) { + + &:hover { + + @mixin facetwp-filter-bar-flyout-toggle-hover-focus; + } + } + + &:focus:not(:focus-visible) { + + @mixin facetwp-filter-bar-flyout-toggle-hover-focus; + } + + &:focus-visible { + + @mixin focus-visible; + } +} + +.b-facetwp-filter-bar__trigger-icon { + display: block; + width: var(--trigger-icon-size); + height: var(--trigger-icon-size); + flex-shrink: 0; + background-color: currentcolor; + mask: var(--icon-filter) center no-repeat; + mask-size: contain; +} + +.b-facetwp-filter-bar__trigger-text { + + @mixin t-animated-underline; + @mixin t-body; + font-weight: var(--font-weight-bold); +} + +/* ------------------------------------------------------------------------- + * FacetWP Filter Bar - Sidebar Mobile - Clear All Button + * ------------------------------------------------------------------------- */ + +.b-facetwp-filter-bar__clear-wrap { + display: inline-block; + + &[hidden] { + display: none; + } +} + +/* ------------------------------------------------------------------------- + * FacetWP Filter Bar - Sidebar Mobile - Flyout + * ------------------------------------------------------------------------- */ + +.b-facetwp-filter-bar[data-filter-bar-position="sidebar"] .b-facetwp-filter-bar__flyout { + position: fixed; + inset: calc(var(--spacer-wpadmin) + var(--spacer-40)) 0 0 0; + z-index: 1000; + display: flex; + flex-direction: column; + background-color: var(--color-white); + box-shadow: var(--box-shadow-negative-top); + border-top-left-radius: var(--border-radius-l); + border-top-right-radius: var(--border-radius-l); + overflow: auto; + visibility: hidden; + opacity: 0; + color: var(--color-black); + transition: visibility 0.2s, opacity 0.2s; + + &[aria-hidden="false"].is-open { + visibility: visible; + opacity: 1; + } + + @container facetwp-archive (min-width: 992px) { + position: static; + z-index: auto; + visibility: visible; + opacity: 1; + overflow: visible; + background-color: transparent; + box-shadow: none; + border-top-left-radius: 0; + border-top-right-radius: 0; + color: var(--color-text); + } +} + +/* ------------------------------------------------------------------------- + * FacetWP Filter Bar - Sidebar Mobile - Flyout - Inner + * ------------------------------------------------------------------------- */ + +.b-facetwp-filter-bar__flyout-inner { + display: flex; + flex-direction: column; + min-height: 100%; + flex: 1; + + @container facetwp-archive (min-width: 992px) { + min-height: 0; + } +} + +/* ------------------------------------------------------------------------- + * FacetWP Filter Bar - Sidebar Mobile - Flyout - Header + * ------------------------------------------------------------------------- */ + +.b-facetwp-filter-bar__flyout-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--spacer-20); + flex-shrink: 0; + padding: var(--spacer-30) var(--grid-margin); + border-bottom: 1px solid var(--color-border-secondary); + + @container facetwp-archive (min-width: 992px) { + display: none; + } +} + +/* ------------------------------------------------------------------------- + * FacetWP Filter Bar - Sidebar Mobile - Flyout - Title + * ------------------------------------------------------------------------- */ + +.b-facetwp-filter-bar__flyout-title { + margin-top: 0; + font-weight: var(--font-weight-bold); +} + +/* ------------------------------------------------------------------------- + * FacetWP Filter Bar - Sidebar Mobile - Flyout - Close Button + * ------------------------------------------------------------------------- */ + +.b-facetwp-filter-bar__flyout-close { + --close-icon-size: 24px; + display: inline-flex; + align-items: center; + gap: var(--spacer-10); + padding: 0; + border: 0; + background: none; + cursor: pointer; + color: var(--color-black); + + @media (--has-hover) { + + &:hover { + + @mixin facetwp-filter-bar-flyout-close-hover-focus; + } + } + + &:focus:not(:focus-visible) { + + @mixin facetwp-filter-bar-flyout-close-hover-focus; + } + + &:focus-visible { + + @mixin focus-visible; + } +} + +.b-facetwp-filter-bar__flyout-close-icon { + display: block; + width: var(--close-icon-size); + height: var(--close-icon-size); + flex-shrink: 0; + background-color: currentcolor; + mask: var(--icon-dismiss) center no-repeat; + mask-size: contain; +} + +.b-facetwp-filter-bar__flyout-close-text { + + @mixin t-animated-underline; + @mixin t-body; + font-weight: var(--font-weight-bold); +} + +/* ------------------------------------------------------------------------- + * FacetWP Filter Bar - Sidebar Mobile - Flyout - Body + * ------------------------------------------------------------------------- */ + +.b-facetwp-filter-bar__flyout-body { + flex: 1; + overflow: auto; + scrollbar-color: var(--color-neutral-50) var(--color-neutral-30); + scrollbar-width: thin; + + @container facetwp-archive (min-width: 992px) {; + overflow: visible; + } +} + +/* ------------------------------------------------------------------------- + * FacetWP Filter Bar - Sidebar Mobile - Flyout Footer + * ------------------------------------------------------------------------- */ + +.b-facetwp-filter-bar__flyout-footer { + flex-shrink: 0; + display: flex; + justify-content: center; + align-items: center; + padding: var(--spacer-30) var(--grid-margin); + border-top: 1px solid var(--color-border-secondary); + + @container facetwp-archive (min-width: 992px) { + display: none; + } +} diff --git a/wp-content/themes/core/blocks/tribe/facetwp-filter-bar/view.js b/wp-content/themes/core/blocks/tribe/facetwp-filter-bar/view.js new file mode 100644 index 000000000..7e5b9479b --- /dev/null +++ b/wp-content/themes/core/blocks/tribe/facetwp-filter-bar/view.js @@ -0,0 +1,33 @@ +/** + * @module facetwp-filter-bar + * + * Front-end behavior for the FacetWP Filter Bar block: + * + * - flyout.js — Mobile sidebar: "Search & Refine" trigger, flyout dialog, focus trap, "Clear all". + * - dropdown-actions.js — Top layout: fSelect dropdown action bar (Clear filter / Filter) and keyboard support. + */ + +import { bindFlyoutEvents, updateClearAllVisibility } from './js/flyout'; +import { initDropdownActions } from './js/dropdown-actions'; + +/** + * @function init + * + * @description Bootstraps flyout for each sidebar filter bar and dropdown actions on facetwp-loaded. + */ +const init = () => { + const sidebarBlocks = document.querySelectorAll( + '.b-facetwp-filter-bar[data-filter-bar-position="sidebar"]' + ); + + sidebarBlocks.forEach( ( block ) => { + bindFlyoutEvents( block ); + } ); + + document.addEventListener( 'facetwp-loaded', () => { + initDropdownActions(); + sidebarBlocks.forEach( updateClearAllVisibility ); + } ); +}; + +init(); diff --git a/wp-content/themes/core/blocks/tribe/facetwp-grid/block.json b/wp-content/themes/core/blocks/tribe/facetwp-grid/block.json new file mode 100644 index 000000000..6993af3e4 --- /dev/null +++ b/wp-content/themes/core/blocks/tribe/facetwp-grid/block.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "tribe/facetwp-grid", + "version": "0.1.0", + "title": "FacetWP Grid", + "category": "theme", + "icon": "archive", + "description": "A post grid powered by FacetWP", + "attributes": { + "postType": { + "type": "string", + "default": "post" + }, + "postsPerPage": { + "type": "number", + "default": 12 + }, + "showPagination": { + "type": "boolean", + "default": false + } + }, + "supports": { + "html": false, + "align": [ + "wide", + "grid", + "full" + ], + "inserter": false, + "spacing": { + "margin": false, + "padding": false + } + }, + "textdomain": "tribe", + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./style-index.css", + "render": "file:./render.php" +} diff --git a/wp-content/themes/core/blocks/tribe/facetwp-grid/edit.js b/wp-content/themes/core/blocks/tribe/facetwp-grid/edit.js new file mode 100644 index 000000000..a35d2e13b --- /dev/null +++ b/wp-content/themes/core/blocks/tribe/facetwp-grid/edit.js @@ -0,0 +1,87 @@ +import { __ } from '@wordpress/i18n'; +import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; +import { + PanelBody, + SelectControl, + RangeControl, + ToggleControl, +} from '@wordpress/components'; +import { withSelect } from '@wordpress/data'; +import { ServerSideRender } from '@wordpress/server-side-render'; +import metadata from './block.json'; + +function Edit( { attributes, setAttributes, isSelected, postTypes } ) { + const blockProps = useBlockProps(); + + const { postType, postsPerPage, showPagination } = attributes; + + const filteredPostTypes = postTypes?.filter( + ( type ) => type.visibility.show_in_nav_menus === true + ); + + return ( +
+ + { isSelected && ( + + + ( { + label: type.labels.singular_name, + value: type.slug, + } ) ) } + help={ __( + 'Choose a post type to display in the grid.', + 'tribe' + ) } + onChange={ ( value ) => + setAttributes( { postType: value } ) + } + /> + + setAttributes( { postsPerPage: value } ) + } + /> + + setAttributes( { showPagination: value } ) + } + /> + + + ) } +
+ ); +} + +export default withSelect( ( select, ownProps ) => { + const postTypes = select( 'core' ).getPostTypes(); + + return { + postTypes, + ...ownProps, + }; +} )( Edit ); diff --git a/wp-content/themes/core/blocks/tribe/facetwp-grid/index.js b/wp-content/themes/core/blocks/tribe/facetwp-grid/index.js new file mode 100644 index 000000000..a3596f2f0 --- /dev/null +++ b/wp-content/themes/core/blocks/tribe/facetwp-grid/index.js @@ -0,0 +1,13 @@ +import { registerBlockType } from '@wordpress/blocks'; + +import './style.pcss'; + +import Edit from './edit'; +import metadata from './block.json'; + +registerBlockType( metadata.name, { + /** + * @see ./edit.js + */ + edit: Edit, +} ); diff --git a/wp-content/themes/core/blocks/tribe/facetwp-grid/render.php b/wp-content/themes/core/blocks/tribe/facetwp-grid/render.php new file mode 100644 index 000000000..6c9f28658 --- /dev/null +++ b/wp-content/themes/core/blocks/tribe/facetwp-grid/render.php @@ -0,0 +1,40 @@ + $attributes, + 'block_classes' => 'b-facetwp-grid', +] ); + +$query = $c->get_query(); + +if ( ! $query->have_posts() ) { + return; +} +?> +
esc_attr( $c->get_block_classes() ), 'style' => $c->get_block_styles() ] ); ?>> +
+ have_posts() ) : ?> + the_post(); ?> + get_the_ID(), + ] ); ?> + +
+ should_show_pagination() ) : ?> + get_pagination_facet(); + ?> +
+ + + +
+ +
+ diff --git a/wp-content/themes/core/blocks/tribe/facetwp-grid/style.pcss b/wp-content/themes/core/blocks/tribe/facetwp-grid/style.pcss new file mode 100644 index 000000000..689cddb8d --- /dev/null +++ b/wp-content/themes/core/blocks/tribe/facetwp-grid/style.pcss @@ -0,0 +1,44 @@ +/* ------------------------------------------------------------------------- + * + * Block - FacetWP Grid - Styles - FE / Editor + * + * ------------------------------------------------------------------------- */ + +.b-facetwp-grid { + container: facetwp-grid / inline-size; +} + +/* ------------------------------------------------------------------------- + * FacetWP Grid - Grid + * ------------------------------------------------------------------------- */ + +.b-facetwp-grid__grid { + --post-template-grid-template-columns: 1fr; + display: grid; + gap: var(--spacer-50) var(--grid-gutter); + grid-template-columns: var(--post-template-grid-template-columns); + + @container facetwp-grid (min-width: 768px) { + --post-template-grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + @container facetwp-grid (min-width: 992px) { + --post-template-grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + @container facetwp-grid (min-width: 1275px) { + --post-template-grid-template-columns: repeat(4, minmax(0, 1fr)); + } +} + +/* ------------------------------------------------------------------------- + * FacetWP Grid - Pagination + * ------------------------------------------------------------------------- */ + +.b-facetwp-grid__pagination { + display: flex; + align-items: center; + justify-content: center; + gap: var(--spacer-20); + margin-top: var(--spacer-50); +} diff --git a/wp-content/themes/core/theme.json b/wp-content/themes/core/theme.json index 36d6a3e78..3748e538c 100644 --- a/wp-content/themes/core/theme.json +++ b/wp-content/themes/core/theme.json @@ -17,6 +17,9 @@ "core/accordion-heading", "core/accordion-panel", "tribe/carousel-slide", + "tribe/facetwp-archive", + "tribe/facetwp-filter-bar", + "tribe/facetwp-grid", "tribe/horizontal-tab", "tribe/logo-marquee", "tribe/vertical-tab"