# contact-forms/2.3.0/classes/Element/PostSelect.php

Contact Forms by Cimatti, version 2.3.0. 216 lines.

- Page: https://pluginprobe.com/plugins/contact-forms/2.3.0/code/classes/Element/PostSelect.php
- Raw: https://pluginprobe.com/plugins/contact-forms/2.3.0/raw/classes/Element/PostSelect.php
- Modified: 2026-07-13T10:04:36+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/contact-forms/2.3.0/code/classes/Element/PostSelect.php#L10-L20`.

```php
<?php
/**
 * PostSelect Element for Contact Forms
 *
 * Renders an accessible searchable dropdown for selecting posts/pages.
 * Uses AJAX for lazy loading to handle sites with thousands of posts.
 * WPML compatible - shows only posts from current language.
 *
 * Key features:
 * - AJAX-powered search with pagination
 * - Keyboard accessible (arrow keys, Enter, Escape)
 * - WCAG 2.2 AA / European Accessibility Act compliant
 * - Matches standard .pfbc-select styling
 *
 * @package Contact Forms
 * @subpackage Element
 * @since 2.0.0-beta.29
 */

// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- PFBC framework extension
class AccuaForm_Element_PostSelect extends AccuaForm_OptionElement {
  
  /**
   * @var string Post type to query
   */
  protected $post_type = 'page';
  
  /**
   * @var string Extra query arguments (query string format)
   */
  protected $extra_args = '';
  
  /**
   * @var string AJAX URL for fetching posts
   */
  protected $ajax_url = '';
  
  /**
   * @var string Nonce for AJAX requests
   */
  protected $nonce = '';
  
  /**
   * Default attributes
   */
  protected $attributes = array('class' => 'pfbc-select pfbc-post-select');

  /**
   * The form instance is serialized into a transient at render time and
   * restored on submit. The parent OptionElement::__sleep() whitelist would
   * drop this element's own properties, so submit-time validation would see
   * the defaults (post_type 'page', no extra_args) instead of the configured
   * values. Keep them in the serialized representation.
   *
   * @since 2.2.27
   */
  public function __sleep() {
    return array('attributes', 'label', 'validation', 'options', 'post_type', 'extra_args', 'ajax_url', 'nonce');
  }

  /**
   * Constructor
   *
   * @param string      $label      The field label.
   * @param string      $name       The field name attribute.
   * @param string      $post_type  Post type to query (default: 'page').
   * @param string      $extra_args Extra query arguments in query string format.
   * @param array|null  $properties Optional element properties.
   */
  public function __construct($label, $name, $post_type = 'page', $extra_args = '', ?array $properties = null) {
    // Initialize with empty options - will be loaded via AJAX
    parent::__construct($label, $name, array(), $properties);
    
    $this->post_type = $post_type;
    $this->extra_args = $extra_args;
    $this->ajax_url = admin_url('admin-ajax.php');
    $this->nonce = wp_create_nonce('accua_forms_get_posts');
    
    // Add data attributes for JavaScript
    $this->attributes['data-post-type'] = $this->post_type;
    $this->attributes['data-ajax-url'] = $this->ajax_url;
    $this->attributes['data-nonce'] = $this->nonce;
    if (!empty($this->extra_args)) {
      $this->attributes['data-extra-args'] = $this->extra_args;
    }
  }

  /**
   * Render the post select element
   *
   * Outputs a native select that will be enhanced by JavaScript for search/AJAX.
   * Falls back to a working select if JavaScript is disabled (with initial options).
   */
  public function render() {
    $this->applyAriaAttributes();
    
    // Get current value
    $value = '';
    if (isset($this->attributes['value'])) {
      $value = is_array($this->attributes['value']) ? reset($this->attributes['value']) : $this->attributes['value'];
    }
    
    // Store selected value for AJAX to include in first page
    if (!empty($value)) {
      $this->attributes['data-selected'] = $value;
    }
    
    // Generate unique ID for this element (access attributes array directly)
    $id = isset($this->attributes['id']) ? $this->attributes['id'] : '';
    if (empty($id)) {
      $id = 'pfbc-post-select-' . uniqid();
      $this->attributes['id'] = $id;
    }
    
    // Start rendering
    echo '<div class="pfbc-post-select-wrapper" data-enhanced="false">';
    
    // Native select element (will be hidden when JS enhances it)
    echo '<select', $this->getAttributes(array('value', 'selected', 'data-post-type', 'data-ajax-url', 'data-nonce', 'data-extra-args', 'data-selected')), '>';
    
    // Empty option first - empty text like Country field does (floating label shows the field name)
    echo '<option value=""></option>';
    
    // If we have a selected value, fetch and render that post.
    // Only posts of the configured type and of a status the field may expose
    // (publish, plus private when explicitly configured): the value can come
    // from user-submitted data (form re-render after a validation error), so
    // this must not disclose titles of other draft/private posts.
    if (!empty($value) && is_numeric($value)) {
      $selected_post = get_post(absint($value));
      if ($selected_post && $selected_post->post_type === $this->getEffectivePostType() && in_array($selected_post->post_status, $this->getAllowedPostStatuses(), true)) {
        echo '<option value="', esc_attr($selected_post->ID), '" selected="selected">', esc_html($selected_post->post_title), '</option>';
      }
    }
    
    echo '</select>';
    
    // Hidden data attributes for JS
    echo '<input type="hidden" class="pfbc-post-select-config"';
    echo ' data-post-type="', esc_attr($this->post_type), '"';
    echo ' data-ajax-url="', esc_attr($this->ajax_url), '"';
    echo ' data-nonce="', esc_attr($this->nonce), '"';
    if (!empty($this->extra_args)) {
      echo ' data-extra-args="', esc_attr($this->extra_args), '"';
    }
    if (!empty($value)) {
      echo ' data-selected="', esc_attr($value), '"';
    }
    echo ' />';
    
    echo '</div>';
  }
  
  /**
   * Get the post type for this select
   *
   * @return string
   */
  public function getPostType() {
    return $this->post_type;
  }

  /**
   * Get the post type actually queried, honoring the post_type override
   * that extra_args may contain (mirrors accua_forms_ajax_get_posts()).
   *
   * @since 2.2.27
   * @return string
   */
  public function getEffectivePostType() {
    if (!empty($this->extra_args)) {
      $extra = array();
      wp_parse_str($this->extra_args, $extra);
      if (!empty($extra['post_type'])) {
        $override = sanitize_text_field($extra['post_type']);
        $valid_post_types = get_post_types(array('public' => true));
        if (isset($valid_post_types[$override])) {
          return $override;
        }
      }
    }
    return $this->post_type;
  }

  /**
   * Get the post statuses this field may expose, honoring an explicit
   * post_status in extra_args (limited to publish/private).
   *
   * @since 2.2.27
   * @return array
   */
  public function getAllowedPostStatuses() {
    if (!empty($this->extra_args) && function_exists('accua_forms_filter_field_post_status')) {
      $extra = array();
      wp_parse_str($this->extra_args, $extra);
      if (!empty($extra['post_status'])) {
        $statuses = accua_forms_filter_field_post_status($extra['post_status']);
        if (!empty($statuses)) {
          return $statuses;
        }
      }
    }
    return array('publish');
  }

  /**
   * Get the extra query arguments
   *
   * @return string
   */
  public function getExtraArgs() {
    return $this->extra_args;
  }
}
// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped

```
