'taxonomy', 'source' => $taxonomy['slug'], 'label' => $taxonomy['label'], 'valueType' => 'term', 'terms' => $taxonomy['terms']]; } return $sources; } function native_source_capabilities($sources) { $result = []; foreach ($sources as $id => $source) $result[$id] = ['choice' => $source['type'] === 'taxonomy' && (!empty($source['terms']) || taxonomy_exists($source['source'] ?? '')), 'number' => false, 'date' => false, 'sort' => false, 'storageFormat' => '', 'numeric' => false]; return $result; } function get_control_choices($filter, $sources) { $result = []; $source = $sources[$filter['id'] ?? ''] ?? []; if (Exclusions\is_source_excluded($source['type'] ?? '', $source['source'] ?? '')) return []; $selected = array_map('strval', \Fqbpro\Plugin\Compatibility\Languages\term_values((array) ($filter['values'] ?? []), $source['source'] ?? '')); foreach ($sources[$filter['id'] ?? '']['terms'] ?? [] as $term) { $value = (string) $term['slug']; if (Exclusions\is_term_excluded($source['source'] ?? '', $value)) continue; if (($filter['choiceMode'] ?? '') === 'selected' && !in_array($value, $selected, true)) continue; $result[] = ['value' => $value, 'label' => $term['name'], 'count' => (int) $term['count']]; } if (isset($filter['order'])) { [$mode, $direction] = array_pad(explode('-', $filter['order'], 2), 2, 'asc'); usort($result, static function ($left, $right) use ($mode, $direction) { $a = (string) $left[$mode === 'text' ? 'label' : 'value']; $b = (string) $right[$mode === 'text' ? 'label' : 'value']; $comparison = $mode === 'number' ? ((float) $a <=> (float) $b) : ($mode === 'date' ? ((strtotime($a) ?: 0) <=> (strtotime($b) ?: 0)) : strnatcasecmp($a, $b)); return $comparison * ($direction === 'desc' ? -1 : 1); }); } return $result; } function normalize_native_controls($raw_controls, $sources) { $sources = native_taxonomy_sources($sources, $raw_controls); $result = $seen = $singletons = []; $capabilities = native_source_capabilities($sources); foreach (array_slice(is_array($raw_controls) ? $raw_controls : [], 0, 48) as $raw) { if (!is_array($raw)) continue; $id = $raw['controlId'] ?? ''; $kind = $raw['kind'] ?? ''; if (!is_string($id) || !preg_match('/^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/', $id) || isset($seen[$id]) || !in_array($kind, ['choice', 'search', 'date', 'number', 'sort', 'apply', 'reset'], true)) continue; $seen[$id] = true; if (in_array($kind, ['search', 'sort'], true) && isset($singletons[$kind])) continue; $singletons[$kind] = true; $source_id = is_string($raw['sourceId'] ?? null) ? $raw['sourceId'] : ''; $source = $sources[$source_id] ?? []; $settings = is_array($raw['settings'] ?? null) ? $raw['settings'] : []; if ($kind === 'choice') $settings = native_choice_settings($settings, $raw); $default_label = ['apply' => __('Apply filters', 'accomplice-query-loop-filters'), 'reset' => __('Reset filters', 'accomplice-query-loop-filters'), 'search' => __('Search', 'accomplice-query-loop-filters'), 'sort' => __('Sort by', 'accomplice-query-loop-filters'), 'date' => __('Date', 'accomplice-query-loop-filters'), 'number' => __('Number', 'accomplice-query-loop-filters'), 'choice' => $source['label'] ?? __('Categories', 'accomplice-query-loop-filters')][$kind]; $control = ['controlId' => $id, 'kind' => $kind, 'sourceId' => $source_id, 'label' => normalize_text($raw['label'] ?? '', $default_label), ...(is_string($raw['urlParameter'] ?? null) && $raw['urlParameter'] !== '' ? ['urlParameter' => $raw['urlParameter']] : []), 'settings' => [], 'errors' => [], 'valueType' => $source['valueType'] ?? '', 'storageFormat' => '']; if (!in_array($kind, ['choice', 'apply', 'reset'], true) || ($kind === 'choice' && !str_starts_with($source_id, 'taxonomy:'))) $control['errors'][] = 'requires-pro'; if ($kind === 'choice') { if (str_starts_with($source_id, 'taxonomy:') && empty($capabilities[$source_id]['choice'])) $control['errors'][] = 'unsupported-source'; $presentation = in_array($settings['presentation'] ?? '', ['dropdown', 'checkbox', 'radio', 'buttons'], true) ? $settings['presentation'] : 'dropdown'; $choices = array_slice(array_values(array_unique(array_map('strval', array_filter((array) ($settings['choices'] ?? []), 'is_scalar')))), 0, 100); $choice_mode = ($settings['choiceMode'] ?? '') === 'selected' || $choices ? 'selected' : 'all'; $control['settings'] = ['presentation' => $presentation, 'multiple' => $presentation !== 'radio' && ($settings['multiple'] ?? false) === true, 'operator' => ($settings['operator'] ?? '') === 'AND' ? 'AND' : 'IN', 'includeChildren' => ($settings['includeChildren'] ?? true) !== false, 'choices' => $choices]; if ($choice_mode === 'selected' && !$choices) $control['settings']['choiceMode'] = 'selected'; foreach (['searchable', 'showCount', 'showEmpty'] as $flag) if (($settings[$flag] ?? false) === true) $control['settings'][$flag] = true; if (in_array($settings['choiceStyle'] ?? '', ['text', 'outlined', 'pill', 'underline'], true)) $control['settings']['choiceStyle'] = $settings['choiceStyle']; if (in_array($settings['order'] ?? '', ['text-asc', 'text-desc', 'number-asc', 'number-desc', 'date-asc', 'date-desc'], true)) $control['settings']['order'] = $settings['order']; $control['choices'] = get_control_choices(['id' => $source_id, 'choiceMode' => $choice_mode, 'values' => $choices, 'order' => $control['settings']['order'] ?? 'text-asc'], $sources); } $control['errors'] = array_values(array_unique($control['errors'])); $result[] = $control; } return $result; } function normalize_configuration($attributes, $post_type, $query_id = 0, $catalog_sources = null) { $attributes = is_array($attributes) ? $attributes : []; $behavior = is_array($attributes['behavior'] ?? null) ? $attributes['behavior'] : []; return ['nativeVersion' => 1, 'queryId' => absint($query_id), 'postType' => sanitize_key($post_type), 'nativeControls' => normalize_native_controls($attributes['nativeControls'] ?? [], Exclusions\filter_sources(is_array($catalog_sources) ? $catalog_sources : get_catalog_sources($post_type))), 'errors' => !empty($attributes['_fqbpUnavailableConfiguration']) ? ['requires-pro'] : [], 'labels' => ['filterBy' => normalize_text($attributes['labels']['filterBy'] ?? '', __('Filter by', 'accomplice-query-loop-filters'))], 'behavior' => ['autoApply' => ($behavior['autoApply'] ?? true) !== false, 'searchDelay' => min(2000, max(200, absint($behavior['searchDelay'] ?? 500)))], ]; } function resolve_header_attributes($block) { $block = upgrade_v4_header_block($block); $attributes = $block['attrs'] ?? []; $attributes['nativeControls'] = native_controls_from_tree($block); return $attributes; } /** Old v4 attributes are mapped to native descriptors without rewriting saved content. */ function upgrade_v4_header_block($block) { if (!is_array($block) || (int) ($block['attrs']['nativeVersion'] ?? 0) === 1) return $block; $attrs = is_array($block['attrs'] ?? null) ? $block['attrs'] : []; $children = $counts = []; $make = static function ($kind, $label, $source = '', $settings = []) use (&$children, &$counts) { $counts[$kind] = ($counts[$kind] ?? 0) + 1; $children[] = ['blockName' => CHILD_CONTROL_PREFIX . $kind, 'attrs' => ['controlId' => 'c-' . $kind . '-' . $counts[$kind], 'label' => $label, 'sourceId' => $source, 'settings' => $settings], 'innerBlocks' => [], 'innerHTML' => '', 'innerContent' => []]; }; // Older flat child compositions are retained for Pro to resolve. Never turn a // saved restriction that cannot be read here into an unrestricted query. if (!empty($attrs['useChildBlocks'])) $attrs['_fqbpUnavailableConfiguration'] = true; foreach (array_slice((array) ($attrs['filters'] ?? []), 0, 24) as $filter) { if (!is_array($filter)) continue; $source = $filter['id'] ?? (($filter['type'] ?? '') . ':' . ($filter['source'] ?? '')); $multiple = empty($attrs['behavior']['singleSelect']) && ($attrs['behavior']['multipleSelections'] ?? ($filter['multiple'] ?? false)); $make('choice', $filter['label'] ?? $source, $source, ['presentation' => ($attrs['display']['presentation'] ?? 'dropdown') === 'dropdown' ? 'dropdown' : ($multiple ? 'checkbox' : 'buttons'), 'multiple' => (bool) $multiple, 'operator' => $filter['operator'] ?? 'IN', 'includeChildren' => $filter['includeChildren'] ?? true, 'choices' => ($filter['choiceMode'] ?? '') === 'selected' ? ($filter['values'] ?? []) : [], ...(($filter['choiceMode'] ?? '') === 'selected' ? ['choiceMode' => 'selected'] : [])]); } if (!empty($attrs['dateFilter']['enabled'])) $make('date', $attrs['dateFilter']['label'] ?? __('Date', 'accomplice-query-loop-filters'), 'post:date', ['mode' => ($attrs['dateFilter']['mode'] ?? '') === 'fields' ? 'field' : 'publish', 'selection' => 'range']); if (!empty($attrs['sorting']['enabled'])) { $options = []; foreach ((array) ($attrs['sorting']['options'] ?? []) as $id) { if (!in_array($id, ['date-desc', 'date-asc', 'title-asc', 'title-desc'], true)) continue; [$column, $order] = explode('-', $id); $options[] = ['id' => $id, 'label' => $id, 'sourceId' => 'post:' . $column, 'order' => $order]; } if (!empty($attrs['sorting']['customOptions'])) $attrs['_fqbpUnavailableConfiguration'] = true; $make('sort', $attrs['sorting']['label'] ?? __('Sort by', 'accomplice-query-loop-filters'), '', ['options' => $options]); } if (($attrs['search']['enabled'] ?? true) !== false) $make('search', $attrs['labels']['search'] ?? __('Search', 'accomplice-query-loop-filters'), '', ['columns' => $attrs['search']['columns'] ?? [], 'placeholder' => $attrs['search']['placeholder'] ?? '']); if (($attrs['behavior']['autoApply'] ?? true) === false) $make('apply', __('Apply filters', 'accomplice-query-loop-filters')); if (($attrs['display']['showReset'] ?? true) !== false) $make('reset', __('Reset', 'accomplice-query-loop-filters')); $block['attrs'] = array_merge($attrs, ['nativeVersion' => 1]); $block['innerBlocks'] = $children; $block['innerHTML'] = ''; $block['innerContent'] = array_fill(0, count($children), null); return $block; }