['name' => __('Finders', 'facetcore'), 'singular_name' => __('Finder', 'facetcore'), 'menu_name' => __('FacetCore', 'facetcore'), 'add_new' => __('Add New', 'facetcore'), 'add_new_item' => __('Add Finder', 'facetcore'), 'edit_item' => __('Edit Finder', 'facetcore'), 'all_items' => __('Finders', 'facetcore'), 'search_items' => __('Search Finders', 'facetcore'), 'not_found' => __('No finders yet. Add your first Finder.', 'facetcore')], 'public' => false, 'show_ui' => true, 'show_in_menu' => true, 'show_in_rest' => false, 'supports' => ['title'], 'menu_icon' => 'dashicons-filter', 'map_meta_cap' => false, 'capabilities' => array_fill_keys(['edit_post', 'read_post', 'delete_post', 'edit_posts', 'edit_others_posts', 'publish_posts', 'read_private_posts', 'delete_posts', 'delete_private_posts', 'delete_published_posts', 'delete_others_posts', 'edit_private_posts', 'edit_published_posts', 'create_posts'], 'manage_woocommerce'), ]); } public function assets(): void { if (!get_current_screen() || get_current_screen()->post_type !== TableConfiguration::TYPE) { return; } wp_enqueue_script('facetcore-admin', plugins_url('assets/admin.js', FACETCORE_FILE), [], FACETCORE_VERSION, true); wp_localize_script('facetcore-admin', 'facetcoreAdminL10n', [ 'noColumns' => __('No columns selected', 'facetcore'), 'gridDefault' => __('Title, Image & Price (default)', 'facetcore'), 'sku' => __('SKU', 'facetcore'), /* translators: %1$s: new position number in the list. */ 'moved' => __('Moved to position %1$s.', 'facetcore'), 'copied' => __('Copied!', 'facetcore'), 'copiedStatus' => __('Copied to clipboard.', 'facetcore'), 'copyManually' => __('Press Ctrl+C / Cmd+C to copy.', 'facetcore'), /* translators: %1$s: maximum number of attributes. */ 'limitStatus' => __('Choose up to %1$s attributes.', 'facetcore'), /* translators: %1$s: maximum number of attributes. */ 'limitAlert' => __('Choose up to %1$s attributes for this section.', 'facetcore'), ]); wp_enqueue_style('facetcore-admin', plugins_url('assets/admin.css', FACETCORE_FILE), [], FACETCORE_VERSION); do_action('facetcore_admin_assets'); } public function boxes(): void { add_meta_box('facetcore-config', __('Finder configuration', 'facetcore'), [$this, 'render'], TableConfiguration::TYPE, 'normal', 'high'); add_meta_box('facetcore-shortcode', __('Shortcode', 'facetcore'), [$this, 'shortcode'], TableConfiguration::TYPE, 'side', 'high'); } public function shortcode(\WP_Post $post): void { if ($post->post_status !== 'publish') { echo '

' . esc_html__('Publish this finder to use its shortcode on a page.', 'facetcore') . '

'; return; } echo '
'; echo '

' . esc_html__('Paste this shortcode into any WordPress page or post.', 'facetcore') . '

'; $this->copyControl($post->ID); echo '
'; } private function copyControl(int $id): void { echo ''; echo ' '; echo ''; echo ''; echo ''; } public function columns(array $columns): array { return ['cb' => $columns['cb'], 'title' => __('Name', 'facetcore'), 'facetcore_shortcode' => __('Shortcode', 'facetcore'), 'facetcore_status' => __('Status', 'facetcore'), 'date' => __('Date', 'facetcore')]; } public function column(string $column, int $id): void { if ($column === 'facetcore_shortcode' && get_post_status($id) === 'publish') { $this->copyControl($id); } if ($column === 'facetcore_status') { echo esc_html(get_post_status_object(get_post_status($id))->label); } } public function render(\WP_Post $post): void { $config = TableConfiguration::get($post->ID, false) ?? TableConfiguration::defaults(); do_action('facetcore_admin_finder', $post); wp_nonce_field('facetcore_config_' . $post->ID, 'facetcore_config_nonce'); echo '
'; if (TableConfiguration::missing($config)) { echo '

' . esc_html__('Some selected attributes no longer exist. They are marked below and safely skipped on the frontend. Uncheck them to remove them.', 'facetcore') . '

'; } // 1. Products $isCategories = $config['source'] === 'categories'; echo '
'; echo '
'; echo '

' . esc_html__('Products', 'facetcore') . '

'; echo '

' . esc_html__('Choose which products this Finder can search.', 'facetcore') . '

'; echo '
'; echo '
'; echo '
'; echo ''; echo ''; echo '
'; // .fc-source-radios $categories = get_terms(['taxonomy' => 'product_cat', 'hide_empty' => false]); echo ''; // .fc-category-picker echo '
'; // .fc-card__body echo '
'; // .fc-card--products // Attributes lookup $attributes = array_map(static fn ($attr) => $attr['label'] . ' (' . $attr['taxonomy'] . ')', TableConfiguration::attributes()); // 2. Filters echo '
'; echo '
'; echo '

' . esc_html__('Filters', 'facetcore') . '

'; echo '

' . esc_html__('Choose WooCommerce attributes customers can filter by.', 'facetcore') . '

'; echo '
'; echo '
'; if (!$attributes) { echo '
'; echo ''; echo '
'; echo '

' . esc_html__('No global WooCommerce attributes found.', 'facetcore') . '

'; echo '

' . esc_html__('To use FacetCore filters:', 'facetcore') . '

'; echo '
    '; echo '
  1. ' . esc_html__('Create global product attributes in WooCommerce.', 'facetcore') . '
  2. '; echo '
  3. ' . esc_html__('Add attribute values.', 'facetcore') . '
  4. '; echo '
  5. ' . esc_html__('Assign them to products.', 'facetcore') . '
  6. '; echo '
  7. ' . esc_html__('Return here and select them as filters.', 'facetcore') . '
  8. '; echo '
'; echo '

'; echo '' . esc_html__('Configure WooCommerce attributes', 'facetcore') . ' →'; echo '

'; echo '
'; echo '
'; } else { $this->choices('filters', $config['filters'], $attributes, 10); } echo '
'; echo '
'; // 3. Results echo '
'; echo '
'; echo '

' . esc_html__('Results', 'facetcore') . '

'; echo '

' . esc_html__('Choose how matching products are displayed.', 'facetcore') . '

'; echo '
'; echo '
'; // Integrated Search toggle echo '
'; echo ''; echo '
'; do_action('facetcore_admin_layout', $config, $post); echo '
'; // .fc-card__body echo '
'; // .fc-card--results // 4. Advanced settings (collapsible, collapsed by default) echo '
'; echo ''; echo '
'; echo '

' . esc_html__('Advanced settings', 'facetcore') . '

'; echo '

' . esc_html__('Fine-tune columns, cards, counts, pagination and view switching.', 'facetcore') . '

'; echo '
'; echo ''; echo '' . esc_html__('Configure', 'facetcore') . ' '; echo ''; echo ''; echo '
'; echo '
'; do_action('facetcore_admin_advanced', $config, $post, $attributes, $this); echo '

'; // 4d. Pagination, counts & filter behavior echo '
'; echo '

' . esc_html__('Pagination & counts', 'facetcore') . '

'; echo '
'; echo '
'; echo ''; echo '

' . esc_html__('Number of products shown per page in Table and Grid views.', 'facetcore') . '

'; echo '
'; echo '
'; echo ' '; echo '' . esc_html__('products (1–100, default: 25)', 'facetcore') . ''; echo '
'; echo '
'; echo '
'; echo '
'; echo ''; echo '

' . esc_html__('Displays product counts next to each filter value (e.g. "DDR4 (16)").', 'facetcore') . '

'; echo '
'; echo '
'; echo '
'; echo '
'; echo ''; echo '

' . esc_html__('Hides filter options that have zero matching products. When unchecked, zero-count options remain visible but disabled.', 'facetcore') . '

'; echo '
'; echo '
'; echo ''; echo '
'; // .fc-advanced-settings-section echo '
'; // .fc-card__body echo '
'; // .fc-card--advanced echo '
'; // .facetcore-editor-wrap } public function choices(string $kind, array $selected, array $options, int $limit = 0): void { $keys = array_unique(array_merge($selected, array_keys($options))); $checkedCount = count(array_intersect($selected, $keys)); echo '
'; echo '
'; if ($limit > 0) { echo '' . (int) $checkedCount . ' / ' . (int) $limit . ' ' . esc_html__('selected', 'facetcore') . ''; } else { echo '' . (int) $checkedCount . ' ' . esc_html__('selected', 'facetcore') . ''; } $hint = $kind === 'columns' ? __('Arrange from left to right.', 'facetcore') : __('Move rows up or down to set display order.', 'facetcore'); echo '' . esc_html($hint) . ''; echo '
'; echo '

'; echo '
'; } public function save(int $id, \WP_Post $post): void { if (wp_is_post_revision($id) || wp_is_post_autosave($id) || $post->post_type !== TableConfiguration::TYPE || !current_user_can('manage_woocommerce') || !current_user_can('edit_post', $id)) { return; } $nonce = isset($_POST['facetcore_config_nonce']) && is_string($_POST['facetcore_config_nonce']) ? sanitize_text_field(wp_unslash($_POST['facetcore_config_nonce'])) : ''; if ($nonce === '' || !wp_verify_nonce($nonce, 'facetcore_config_' . $id)) { return; } $raw = isset($_POST['facetcore_config']) && is_array($_POST['facetcore_config']) ? self::input(wp_unslash($_POST['facetcore_config'])) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized field by field in self::input(). if (!is_array($raw)) { return; } foreach (['enable_search', 'grid_sku'] as $key) { $raw[$key] = $raw[$key] ?? false; } $old = TableConfiguration::get($id, false) ?? TableConfiguration::defaults(); $config = TableConfiguration::forSave($raw, $old); // Reject forged new attribute references, but retain already-saved missing ones for the warning UI. $allowed = array_merge(array_keys(TableConfiguration::attributes()), $old['filters']); $config['filters'] = array_values(array_intersect($config['filters'], $allowed)); $config = apply_filters('facetcore_validate_save_configuration', $config, $old); $config = TableConfiguration::forSave($config, $old); try { (new AttributeIndex())->ensure($config); } catch (\Throwable $error) { wp_die(esc_html__('The attribute index could not be updated. Configuration was not saved. Please retry.', 'facetcore')); } update_post_meta($id, TableConfiguration::META, $config); } /** Per-field input sanitization of the unslashed form array; unknown keys are dropped. */ private static function input(array $raw): array { $out = []; foreach (['source', 'enable_search', 'grid_sku', 'show_counts', 'hide_impossible', 'per_page'] as $key) { if (isset($raw[$key]) && is_scalar($raw[$key])) { $out[$key] = sanitize_text_field((string) $raw[$key]); } } foreach (['filters', 'categories'] as $key) { if (isset($raw[$key]) && is_array($raw[$key])) { $out[$key] = array_values(array_map(static fn ($item): string => sanitize_text_field((string) $item), array_filter($raw[$key], 'is_scalar'))); } } return apply_filters('facetcore_admin_input', $out, $raw); } }