PluginProbe
Contact Forms by Cimatti / 2.3.5
Contact Forms by Cimatti v2.3.5
2.3.6 2.3.5 2.3.0 2.2.32 2.2.4 2.2.0 2.1.2 2.1.1 trunk 1.0 1.1 1.2 1.2.1 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 All 62 releases
contact-forms / classes / Element / PostSelect.php

PostSelect.php in Contact Forms by Cimatti 2.3.5, at classes/Element/PostSelect.php

216 lines 7.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * PostSelect Element for Contact Forms
4 *
5 * Renders an accessible searchable dropdown for selecting posts/pages.
6 * Uses AJAX for lazy loading to handle sites with thousands of posts.
7 * WPML compatible - shows only posts from current language.
8 *
9 * Key features:
10 * - AJAX-powered search with pagination
11 * - Keyboard accessible (arrow keys, Enter, Escape)
12 * - WCAG 2.2 AA / European Accessibility Act compliant
13 * - Matches standard .pfbc-select styling
14 *
15 * @package Contact Forms
16 * @subpackage Element
17 * @since 2.0.0-beta.29
18 */
19
20 // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- PFBC framework extension
21 class AccuaForm_Element_PostSelect extends AccuaForm_OptionElement {
22
23 /**
24 * @var string Post type to query
25 */
26 protected $post_type = 'page';
27
28 /**
29 * @var string Extra query arguments (query string format)
30 */
31 protected $extra_args = '';
32
33 /**
34 * @var string AJAX URL for fetching posts
35 */
36 protected $ajax_url = '';
37
38 /**
39 * @var string Nonce for AJAX requests
40 */
41 protected $nonce = '';
42
43 /**
44 * Default attributes
45 */
46 protected $attributes = array('class' => 'pfbc-select pfbc-post-select');
47
48 /**
49 * The form instance is serialized into a transient at render time and
50 * restored on submit. The parent OptionElement::__sleep() whitelist would
51 * drop this element's own properties, so submit-time validation would see
52 * the defaults (post_type 'page', no extra_args) instead of the configured
53 * values. Keep them in the serialized representation.
54 *
55 * @since 2.2.27
56 */
57 public function __sleep() {
58 return array('attributes', 'label', 'validation', 'options', 'post_type', 'extra_args', 'ajax_url', 'nonce');
59 }
60
61 /**
62 * Constructor
63 *
64 * @param string $label The field label.
65 * @param string $name The field name attribute.
66 * @param string $post_type Post type to query (default: 'page').
67 * @param string $extra_args Extra query arguments in query string format.
68 * @param array|null $properties Optional element properties.
69 */
70 public function __construct($label, $name, $post_type = 'page', $extra_args = '', ?array $properties = null) {
71 // Initialize with empty options - will be loaded via AJAX
72 parent::__construct($label, $name, array(), $properties);
73
74 $this->post_type = $post_type;
75 $this->extra_args = $extra_args;
76 $this->ajax_url = admin_url('admin-ajax.php');
77 $this->nonce = wp_create_nonce('accua_forms_get_posts');
78
79 // Add data attributes for JavaScript
80 $this->attributes['data-post-type'] = $this->post_type;
81 $this->attributes['data-ajax-url'] = $this->ajax_url;
82 $this->attributes['data-nonce'] = $this->nonce;
83 if (!empty($this->extra_args)) {
84 $this->attributes['data-extra-args'] = $this->extra_args;
85 }
86 }
87
88 /**
89 * Render the post select element
90 *
91 * Outputs a native select that will be enhanced by JavaScript for search/AJAX.
92 * Falls back to a working select if JavaScript is disabled (with initial options).
93 */
94 public function render() {
95 $this->applyAriaAttributes();
96
97 // Get current value
98 $value = '';
99 if (isset($this->attributes['value'])) {
100 $value = is_array($this->attributes['value']) ? reset($this->attributes['value']) : $this->attributes['value'];
101 }
102
103 // Store selected value for AJAX to include in first page
104 if (!empty($value)) {
105 $this->attributes['data-selected'] = $value;
106 }
107
108 // Generate unique ID for this element (access attributes array directly)
109 $id = isset($this->attributes['id']) ? $this->attributes['id'] : '';
110 if (empty($id)) {
111 $id = 'pfbc-post-select-' . uniqid();
112 $this->attributes['id'] = $id;
113 }
114
115 // Start rendering
116 echo '<div class="pfbc-post-select-wrapper" data-enhanced="false">';
117
118 // Native select element (will be hidden when JS enhances it)
119 echo '<select', $this->getAttributes(array('value', 'selected', 'data-post-type', 'data-ajax-url', 'data-nonce', 'data-extra-args', 'data-selected')), '>';
120
121 // Empty option first - empty text like Country field does (floating label shows the field name)
122 echo '<option value=""></option>';
123
124 // If we have a selected value, fetch and render that post.
125 // Only posts of the configured type and of a status the field may expose
126 // (publish, plus private when explicitly configured): the value can come
127 // from user-submitted data (form re-render after a validation error), so
128 // this must not disclose titles of other draft/private posts.
129 if (!empty($value) && is_numeric($value)) {
130 $selected_post = get_post(absint($value));
131 if ($selected_post && $selected_post->post_type === $this->getEffectivePostType() && in_array($selected_post->post_status, $this->getAllowedPostStatuses(), true)) {
132 echo '<option value="', esc_attr($selected_post->ID), '" selected="selected">', esc_html($selected_post->post_title), '</option>';
133 }
134 }
135
136 echo '</select>';
137
138 // Hidden data attributes for JS
139 echo '<input type="hidden" class="pfbc-post-select-config"';
140 echo ' data-post-type="', esc_attr($this->post_type), '"';
141 echo ' data-ajax-url="', esc_attr($this->ajax_url), '"';
142 echo ' data-nonce="', esc_attr($this->nonce), '"';
143 if (!empty($this->extra_args)) {
144 echo ' data-extra-args="', esc_attr($this->extra_args), '"';
145 }
146 if (!empty($value)) {
147 echo ' data-selected="', esc_attr($value), '"';
148 }
149 echo ' />';
150
151 echo '</div>';
152 }
153
154 /**
155 * Get the post type for this select
156 *
157 * @return string
158 */
159 public function getPostType() {
160 return $this->post_type;
161 }
162
163 /**
164 * Get the post type actually queried, honoring the post_type override
165 * that extra_args may contain (mirrors accua_forms_ajax_get_posts()).
166 *
167 * @since 2.2.27
168 * @return string
169 */
170 public function getEffectivePostType() {
171 if (!empty($this->extra_args)) {
172 $extra = array();
173 wp_parse_str($this->extra_args, $extra);
174 if (!empty($extra['post_type'])) {
175 $override = sanitize_text_field($extra['post_type']);
176 $valid_post_types = get_post_types(array('public' => true));
177 if (isset($valid_post_types[$override])) {
178 return $override;
179 }
180 }
181 }
182 return $this->post_type;
183 }
184
185 /**
186 * Get the post statuses this field may expose, honoring an explicit
187 * post_status in extra_args (limited to publish/private).
188 *
189 * @since 2.2.27
190 * @return array
191 */
192 public function getAllowedPostStatuses() {
193 if (!empty($this->extra_args) && function_exists('accua_forms_filter_field_post_status')) {
194 $extra = array();
195 wp_parse_str($this->extra_args, $extra);
196 if (!empty($extra['post_status'])) {
197 $statuses = accua_forms_filter_field_post_status($extra['post_status']);
198 if (!empty($statuses)) {
199 return $statuses;
200 }
201 }
202 }
203 return array('publish');
204 }
205
206 /**
207 * Get the extra query arguments
208 *
209 * @return string
210 */
211 public function getExtraArgs() {
212 return $this->extra_args;
213 }
214 }
215 // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
216