PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents / 4.6.2
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents v4.6.2
4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 trunk 1.0.0 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 2.0.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.2.0 2.2.1 2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.1.3 3.2.0 3.2.1 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.6.8 3.7.0 3.7.1 3.7.2 3.7.3 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.9.0 3.9.1 3.9.10 3.9.11 3.9.12 3.9.13 3.9.14 3.9.15 3.9.16 3.9.17 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9 4.0.0 4.0.1 4.0.10 4.0.11 4.0.12 4.0.13 4.0.14 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.10 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.8 4.2.9 4.3.0 4.3.1 4.4.0 4.4.1 4.4.10 4.4.11 4.4.2 4.4.3 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 4.5.0 4.5.1
embedpress / EmbedPress / Elementor / Widgets / Embedpress_Google_Reviews.php
embedpress / EmbedPress / Elementor / Widgets Last commit date
Embedpress_Calendar.php 2 months ago Embedpress_Document.php 1 month ago Embedpress_Elementor.php 1 month ago Embedpress_Google_Reviews.php 3 weeks ago Embedpress_Pdf.php 3 weeks ago Embedpress_Pdf_Gallery.php 3 weeks ago
Embedpress_Google_Reviews.php
695 lines
1 <?php
2
3 namespace EmbedPress\Elementor\Widgets;
4
5 use Elementor\Controls_Manager;
6 use Elementor\Widget_Base;
7 use EmbedPress\Includes\Classes\GoogleReviewsRenderer;
8 use EmbedPress\Elementor\Controls\Place_Picker as GR_Place_Picker;
9
10 (defined('ABSPATH')) or die("No direct script access allowed.");
11
12 /**
13 * Elementor widget for embedding Google Business reviews.
14 *
15 * v1: Place ID input + a clear pointer to the Settings → Google Reviews picker
16 * for search UX. Elementor's controls API doesn't natively support a remote
17 * autocomplete; rather than ship a fragile custom control in v1 we route users
18 * to the existing searchable picker and copy the place_id back here.
19 *
20 * Output uses the shared GoogleReviewsRenderer so widget + block + shortcode
21 * all emit identical markup.
22 */
23 class Embedpress_Google_Reviews extends Widget_Base
24 {
25 public function get_name()
26 {
27 return 'embedpress-google-reviews';
28 }
29
30 public function get_title()
31 {
32 return esc_html__('EmbedPress Google Reviews', 'embedpress');
33 }
34
35 public function get_categories()
36 {
37 return ['embedpress'];
38 }
39
40 public function get_icon()
41 {
42 return 'eicon-favorite';
43 }
44
45 public function get_keywords()
46 {
47 return ['embedpress', 'google', 'reviews', 'places', 'business', 'rating'];
48 }
49
50 public function get_custom_help_url()
51 {
52 return 'https://embedpress.com/docs/embed-google-reviews-in-wordpress/';
53 }
54
55 /** Pro-control marking (canonical EmbedPress pattern). Empty when Pro is
56 * active (Pro hooks these filters in includes/Filters/Utility.php), so the
57 * greyed overlay + "(Pro)" label drop and the controls become live. */
58 protected $pro_class = '';
59 protected $pro_text = '';
60
61 /**
62 * Per-layout capability matrix — MUST stay identical to LAYOUT_CAPS in the
63 * Gutenberg block (src/Blocks/google-reviews/src/edit.js) and the renderer's
64 * GoogleReviewsRenderer::layout_caps(). Single source of truth for which
65 * controls each layout supports. See the JS copy for the flag meanings.
66 */
67 // Keep identical to LAYOUT_CAPS in GoogleReviewsRenderer.php + edit.js.
68 const LAYOUT_CAPS = [
69 'list' => ['reviews' => true, 'header' => 'optional', 'columns' => false, 'gap' => false, 'max_width' => true, 'slider' => false, 'autoplay' => false, 'speed' => false, 'load_more' => true, 'images' => true, 'write_review' => true],
70 'grid' => ['reviews' => true, 'header' => 'optional', 'columns' => true, 'gap' => true, 'max_width' => true, 'slider' => false, 'autoplay' => false, 'speed' => false, 'load_more' => true, 'images' => true, 'write_review' => true],
71 'card' => ['reviews' => true, 'header' => 'optional', 'columns' => true, 'gap' => true, 'max_width' => true, 'slider' => false, 'autoplay' => false, 'speed' => false, 'load_more' => true, 'images' => true, 'write_review' => true],
72 'carousel' => ['reviews' => true, 'header' => 'optional', 'columns' => true, 'gap' => true, 'max_width' => true, 'slider' => true, 'autoplay' => true, 'speed' => false, 'load_more' => false, 'images' => false, 'write_review' => true],
73 'masonry' => ['reviews' => true, 'header' => 'optional', 'columns' => true, 'gap' => true, 'max_width' => true, 'slider' => false, 'autoplay' => false, 'speed' => false, 'load_more' => true, 'images' => true, 'write_review' => true],
74 'badge' => ['reviews' => false, 'header' => 'forced', 'columns' => false, 'gap' => false, 'max_width' => true, 'slider' => false, 'autoplay' => false, 'speed' => false, 'load_more' => false, 'images' => false, 'write_review' => false],
75 'spotlight' => ['reviews' => true, 'header' => 'optional', 'columns' => false, 'gap' => false, 'max_width' => true, 'slider' => false, 'autoplay' => true, 'speed' => false, 'load_more' => false, 'images' => true, 'write_review' => true],
76 'knowledge' => ['reviews' => false, 'header' => 'forced', 'columns' => false, 'gap' => false, 'max_width' => true, 'slider' => false, 'autoplay' => false, 'speed' => false, 'load_more' => false, 'images' => false, 'write_review' => false],
77 'marquee' => ['reviews' => true, 'header' => 'optional', 'columns' => true, 'gap' => true, 'max_width' => true, 'slider' => false, 'autoplay' => false, 'speed' => true, 'load_more' => false, 'images' => false, 'write_review' => true],
78 'bubble' => ['reviews' => true, 'header' => 'optional', 'columns' => true, 'gap' => true, 'max_width' => true, 'slider' => false, 'autoplay' => false, 'speed' => false, 'load_more' => true, 'images' => true, 'write_review' => true],
79 ];
80
81 /** Layouts that render a per-review list (not summary-only). */
82 protected static function review_layouts()
83 {
84 return self::caps_layouts('reviews');
85 }
86
87 /** Layouts whose header is optional (has a "Show header" toggle). */
88 protected static function header_optional_layouts()
89 {
90 $out = [];
91 foreach (self::LAYOUT_CAPS as $slug => $caps) {
92 if (($caps['header'] ?? 'optional') === 'optional') {
93 $out[] = $slug;
94 }
95 }
96 return $out;
97 }
98
99 /** Layouts whose "columns" control is a slides-per-view count, not a grid. */
100 const SLIDES_PER_VIEW_LAYOUTS = ['carousel', 'marquee'];
101
102 /** List of layout slugs whose caps[$flag] is true — for Elementor conditions. */
103 protected static function caps_layouts($flag)
104 {
105 $out = [];
106 foreach (self::LAYOUT_CAPS as $slug => $caps) {
107 if (!empty($caps[$flag])) {
108 $out[] = $slug;
109 }
110 }
111 return $out;
112 }
113
114 /** Resolve the effective columns value — slider layouts use ep_gr_slides. */
115 protected static function columns_arg($s)
116 {
117 $layout = isset($s['ep_gr_layout']) ? sanitize_key($s['ep_gr_layout']) : 'list';
118 if (in_array($layout, self::SLIDES_PER_VIEW_LAYOUTS, true) && isset($s['ep_gr_slides'])) {
119 return (int) $s['ep_gr_slides'];
120 }
121 return isset($s['ep_gr_columns']) ? (int) $s['ep_gr_columns'] : 3;
122 }
123
124 /**
125 * Resolve the speed arg. Marquee uses the 1–10 "Scroll speed" SLIDER (returns
126 * ['size' => N]); carousel/spotlight use the "Autoplay speed" NUMBER (seconds).
127 * Marquee's JS reads this as a 1–10 scale; carousel/spotlight as seconds.
128 */
129 protected static function speed_arg($s)
130 {
131 $layout = isset($s['ep_gr_layout']) ? sanitize_key($s['ep_gr_layout']) : 'list';
132 $caps = self::LAYOUT_CAPS[$layout] ?? self::LAYOUT_CAPS['list'];
133 if (!empty($caps['speed'])) { // marquee — 1–10 slider
134 $size = isset($s['ep_gr_scroll_speed']['size']) ? (int) $s['ep_gr_scroll_speed']['size'] : 4;
135 return max(1, min(10, $size ?: 4));
136 }
137 // carousel / spotlight — seconds
138 return isset($s['ep_gr_autoplay_speed']) && (int) $s['ep_gr_autoplay_speed'] > 0
139 ? (int) $s['ep_gr_autoplay_speed']
140 : 5;
141 }
142
143 protected function register_controls()
144 {
145 // Same primitives the main EmbedPress widget uses: a CSS class that the
146 // free el-icon.css greys out + blocks, and a "(Pro)" label suffix. Pro
147 // returns '' for both, unlocking every control in place.
148 $this->pro_class = apply_filters('embedpress/pro_class', 'embedpress-pro-control not-active');
149 $this->pro_text = apply_filters('embedpress/pro_text', '<sup class="embedpress-pro-label" style="color:red">' . __('(Pro)', 'embedpress') . '</sup>');
150
151 /* ----- Place ----- */
152 $this->start_controls_section('ep_gr_section_place', [
153 'label' => esc_html__('Place', 'embedpress'),
154 ]);
155
156 $this->add_control('ep_gr_place', [
157 'label' => __('Search for a place', 'embedpress'),
158 'type' => GR_Place_Picker::CONTROL_TYPE,
159 'label_block' => true,
160 'default' => ['place_id' => '', 'place_name' => '', 'place_address' => ''],
161 ]);
162
163 // Guidance: the quick preview shows 5 reviews; pulling more is an explicit
164 // Settings action. Kept to one short line with a link. Shown once a place
165 // is set.
166 $gr_settings_url = admin_url('admin.php?page=embedpress-google-reviews');
167 $this->add_control('ep_gr_fetch_hint', [
168 'type' => Controls_Manager::RAW_HTML,
169 'raw' => '<div class="ep-gr-elementor-hint">'
170 . esc_html__('Quick preview — 5 reviews.', 'embedpress')
171 . ' <a href="' . esc_url($gr_settings_url) . '" target="_blank" rel="noopener noreferrer">'
172 . esc_html__('Fetch more', 'embedpress') . '</a>'
173 . '</div>',
174 'content_classes' => 'ep-gr-elementor-hint-wrap',
175 'condition' => ['ep_gr_place[place_id]!' => ''],
176 ]);
177
178 $this->end_controls_section();
179
180 /* Section order: Place → Layout & Display (pick a layout first) →
181 * Reviews & Filtering → Review Elements → Motion → Colors & Theme → SEO
182 * → Sources. Layout sits right after the place search so the filtering /
183 * elements sections below adapt to the chosen layout's capabilities.
184 * (sort/keyword/hide-empty, theme/accent, autoplay, schema, cache are
185 * all FREE now — Pro only gates review images + multi-place + premium
186 * layouts.) */
187 $this->register_display_controls();
188
189 $this->register_pro_filtering_controls();
190
191 /* ----- Header / Summary ----- (business name, rating, star row, count,
192 * alignment). Parity with the Gutenberg block's Header controls. Only for
193 * layouts whose header is optional; badge/knowledge force the header on. */
194 $this->start_controls_section('ep_gr_header', [
195 'label' => esc_html__('Header', 'embedpress'),
196 'condition' => ['ep_gr_layout' => self::header_optional_layouts()],
197 ]);
198
199 $this->add_control('ep_gr_show_summary', [
200 'label' => __('Show header', 'embedpress'),
201 'type' => Controls_Manager::SWITCHER,
202 'default' => 'yes',
203 'label_on' => __('On', 'embedpress'),
204 'label_off' => __('Off', 'embedpress'),
205 'return_value' => 'yes',
206 ]);
207
208 foreach ([
209 'ep_gr_show_summary_name' => __('Business name', 'embedpress'),
210 'ep_gr_show_summary_rating' => __('Rating score', 'embedpress'),
211 'ep_gr_show_summary_stars' => __('Star row', 'embedpress'),
212 'ep_gr_show_summary_count' => __('Review count', 'embedpress'),
213 ] as $key => $label) {
214 $this->add_control($key, [
215 'label' => $label,
216 'type' => Controls_Manager::SWITCHER,
217 'default' => 'yes',
218 'label_on' => __('On', 'embedpress'),
219 'label_off' => __('Off', 'embedpress'),
220 'return_value' => 'yes',
221 // Sub-toggles only matter when the header itself is shown.
222 'condition' => ['ep_gr_show_summary' => 'yes'],
223 ]);
224 }
225
226 $this->add_control('ep_gr_summary_align', [
227 'label' => __('Header alignment', 'embedpress'),
228 'type' => Controls_Manager::CHOOSE,
229 'default' => 'left',
230 'options' => [
231 'left' => ['title' => __('Left', 'embedpress'), 'icon' => 'eicon-text-align-left'],
232 'center' => ['title' => __('Center', 'embedpress'), 'icon' => 'eicon-text-align-center'],
233 'right' => ['title' => __('Right', 'embedpress'), 'icon' => 'eicon-text-align-right'],
234 ],
235 'condition' => ['ep_gr_show_summary' => 'yes'],
236 ]);
237
238 $this->end_controls_section();
239
240 /* ----- Review Elements ----- (per-review toggles — only for layouts that
241 * render a review list; summary-only badge/knowledge show no reviews). */
242 $this->start_controls_section('ep_gr_elements', [
243 'label' => esc_html__('Review Elements', 'embedpress'),
244 'condition' => ['ep_gr_layout' => self::review_layouts()],
245 ]);
246
247 foreach ([
248 'ep_gr_show_photo' => __('Reviewer photo', 'embedpress'),
249 'ep_gr_show_stars' => __('Star rating', 'embedpress'),
250 'ep_gr_show_date' => __('Date', 'embedpress'),
251 'ep_gr_show_images' => __('Review images', 'embedpress'),
252 'ep_gr_show_link' => __('"View on Google" link', 'embedpress'),
253 ] as $key => $label) {
254 $args = [
255 'label' => $label,
256 'type' => Controls_Manager::SWITCHER,
257 // "View on Google" link defaults OFF; the other elements default ON.
258 'default' => $key === 'ep_gr_show_link' ? '' : 'yes',
259 'label_on' => __('On', 'embedpress'),
260 'label_off' => __('Off', 'embedpress'),
261 'return_value' => 'yes',
262 ];
263 // Review images (Pro) only apply to layouts with room for them — hide
264 // the toggle for compact/sliding layouts that never render attached
265 // photos, and gate it behind Pro (greyed + locked until Pro clears the
266 // class).
267 if ($key === 'ep_gr_show_images') {
268 $args['condition'] = ['ep_gr_layout' => self::caps_layouts('images')];
269 $args['label'] = sprintf('%1$s %2$s', $label, $this->pro_text);
270 $args['classes'] = $this->pro_class;
271 }
272 $this->add_control($key, $args);
273 }
274
275 $this->end_controls_section();
276
277 // Colors & Theme → SEO → Sources (Motion is registered earlier, before
278 // Colors, to match the Gutenberg panel order).
279 $this->register_pro_appearance_controls();
280 }
281
282 /**
283 * Layout & Display section — the layout-tile picker + structural controls
284 * (columns/slides/gap/max-width). Registered right after Place so the user
285 * picks a layout first, then the filtering/elements sections below adapt to
286 * the chosen layout's capabilities.
287 */
288 protected function register_display_controls()
289 {
290 /* ----- Layout & Display ----- */
291 $this->start_controls_section('ep_gr_display', [
292 'label' => esc_html__('Layout & Display', 'embedpress'),
293 ]);
294
295 // Icon-tile layout picker — mirrors the Gutenberg block's LayoutPicker.
296 // Uses Controls_Manager::CHOOSE (same pattern Essential Addons uses, e.g.
297 // Post Grid → layout_mode) with native Elementor eicons so the tiles
298 // render crisply + flip correctly in dark/light editor themes. The tile
299 // grid layout is styled in static/css/ep-gr-elementor-picker.css (scoped
300 // to .elementor-control-ep_gr_layout). Pro layouts keep the "(Pro)"
301 // suffix in their tooltip title; selecting one without Pro falls back to
302 // the base layout server-side. When Pro is active $this->pro_text is ''.
303 $pro = ($this->pro_text === '') ? '' : ' ' . wp_strip_all_tags($this->pro_text);
304 // When Pro is inactive, tag the control so the picker CSS can badge the
305 // Pro-only layout tiles with a "PRO" pill. Pro active → no badge.
306 $layout_classes = ($this->pro_text === '') ? '' : 'ep-gr-layout-choose ep-gr-layout-choose--free';
307 $this->add_control('ep_gr_layout', [
308 'label' => __('Layout', 'embedpress'),
309 'type' => Controls_Manager::CHOOSE,
310 'toggle' => false,
311 'default' => 'list',
312 'classes' => $layout_classes,
313 'options' => [
314 'list' => ['title' => __('List', 'embedpress'), 'icon' => 'eicon-post-list'],
315 'grid' => ['title' => __('Grid', 'embedpress'), 'icon' => 'eicon-posts-grid'],
316 'card' => ['title' => __('Card', 'embedpress'), 'icon' => 'eicon-image-box'],
317 'carousel' => ['title' => __('Carousel', 'embedpress'), 'icon' => 'eicon-posts-carousel'],
318 'masonry' => ['title' => __('Masonry', 'embedpress') . $pro, 'icon' => 'eicon-gallery-masonry'],
319 'badge' => ['title' => __('Compact badge', 'embedpress') . $pro, 'icon' => 'eicon-rating'],
320 'spotlight' => ['title' => __('Spotlight', 'embedpress') . $pro, 'icon' => 'eicon-featured-image'],
321 'knowledge' => ['title' => __('Knowledge panel', 'embedpress') . $pro, 'icon' => 'eicon-info-box'],
322 'marquee' => ['title' => __('Marquee (auto-scroll)', 'embedpress') . $pro, 'icon' => 'eicon-posts-ticker'],
323 'bubble' => ['title' => __('Bubble', 'embedpress') . $pro, 'icon' => 'eicon-testimonial'],
324 ],
325 ]);
326
327 // NOTE: Minimum rating, Reviews per page, and "Load more" moved to the
328 // Reviews & Filtering section (register_pro_filtering_controls) — they
329 // filter/limit which reviews show, not the layout. Parity with the block.
330
331 // Structural controls (free), each gated by the capability matrix so a
332 // layout only shows the spacing controls it actually uses. Columns has a
333 // dynamic label: grid-type layouts call it "Columns", sliders call it
334 // "Slides per view". Elementor can't relabel one control by layout, so we
335 // register two — a grid one and a slider one — both feeding the same
336 // render arg (mapped in get_render_args / elementor_render_args).
337 $columns_grid = array_values(array_diff(self::caps_layouts('columns'), self::SLIDES_PER_VIEW_LAYOUTS));
338 $columns_slider = array_values(array_intersect(self::caps_layouts('columns'), self::SLIDES_PER_VIEW_LAYOUTS));
339
340 $this->add_control('ep_gr_columns', [
341 'label' => __('Columns', 'embedpress'),
342 'type' => Controls_Manager::NUMBER,
343 'default' => 3,
344 'min' => 1,
345 'max' => 6,
346 'description' => __('Number of columns. Collapses on smaller screens.', 'embedpress'),
347 'condition' => ['ep_gr_layout' => $columns_grid],
348 ]);
349 $this->add_control('ep_gr_slides', [
350 'label' => __('Slides per view', 'embedpress'),
351 'type' => Controls_Manager::NUMBER,
352 'default' => 3,
353 'min' => 1,
354 'max' => 6,
355 'description' => __('How many review cards are shown at once.', 'embedpress'),
356 'condition' => ['ep_gr_layout' => $columns_slider],
357 ]);
358
359 $this->add_control('ep_gr_gap', [
360 'label' => __('Gap (px)', 'embedpress'),
361 'type' => Controls_Manager::NUMBER,
362 'default' => 20,
363 'min' => 0,
364 'max' => 80,
365 'condition' => ['ep_gr_layout' => self::caps_layouts('gap')],
366 ]);
367
368 // Max width — review-list layouts only (summary-only badge/knowledge are
369 // a single compact card; their width isn't user-controlled here).
370 $this->add_control('ep_gr_max_width', [
371 'label' => __('Max width (px)', 'embedpress'),
372 'type' => Controls_Manager::NUMBER,
373 'default' => 0,
374 'min' => 0,
375 'max' => 1600,
376 'description' => __('Cap the block width and center it. 0 = full width.', 'embedpress'),
377 'condition' => ['ep_gr_layout' => self::review_layouts()],
378 ]);
379
380 $this->end_controls_section();
381 }
382
383 /**
384 * Reviews & Filtering — Pro controls, defined in the free widget and marked
385 * with $this->pro_class so they render greyed + locked until Pro clears the
386 * class. Values map to renderer args in get_render_args().
387 */
388 protected function register_pro_filtering_controls()
389 {
390 $this->start_controls_section('ep_gr_pro_filtering', [
391 'label' => __('Reviews & Filtering', 'embedpress'),
392 // Sort/keyword/hide-empty operate on the review list — review layouts only.
393 'condition' => ['ep_gr_layout' => self::review_layouts()],
394 ]);
395
396 // The controls form a PIPELINE top to bottom: FILTER (min rating, keyword,
397 // hide-empty) -> ORDER (sort) -> SHOW (reviews per page). Descriptions make
398 // that explicit so the controls don't read as overlapping/conflicting.
399
400 // 1. FILTER — Minimum rating (PRO). "Show only 5�
401 " — the highest-demand
402 // filter; exclusive Pro (free ships keyword + hide-empty). Greyed + "(Pro)"
403 // until Pro clears pro_class.
404 $this->add_control('ep_gr_min_rating', [
405 'label' => sprintf('%1$s %2$s', __('Minimum rating', 'embedpress'), $this->pro_text),
406 'type' => Controls_Manager::SELECT,
407 'default' => '0',
408 'classes' => $this->pro_class,
409 'description' => __('Filter: only reviews at or above this rating are eligible to show.', 'embedpress'),
410 'options' => [
411 '0' => __('Any', 'embedpress'),
412 '3' => '3+',
413 '4' => '4+',
414 '5' => '5',
415 ],
416 ]);
417 $this->add_control('ep_gr_keyword', [
418 'label' => __('Keyword filter', 'embedpress'),
419 'type' => Controls_Manager::TEXT,
420 'placeholder' => __('e.g. service, staff, clean', 'embedpress'),
421 'description' => __('Filter: only reviews whose text contains this word.', 'embedpress'),
422 ]);
423 $this->add_control('ep_gr_hide_empty', [
424 'label' => __('Hide reviews with no text', 'embedpress'),
425 'type' => Controls_Manager::SWITCHER,
426 'return_value' => 'yes',
427 ]);
428
429 // 2. ORDER — Sort the eligible (filtered) reviews. FREE.
430 $this->add_control('ep_gr_sort', [
431 'label' => __('Sort by', 'embedpress'),
432 'type' => Controls_Manager::SELECT,
433 'default' => 'newest',
434 'description' => __('Order of the filtered reviews (applied after the filters above).', 'embedpress'),
435 'options' => [
436 'newest' => __('Newest', 'embedpress'),
437 'highest' => __('Highest rated', 'embedpress'),
438 'lowest' => __('Lowest rated', 'embedpress'),
439 'relevant' => __('Most relevant', 'embedpress'),
440 ],
441 ]);
442
443 // 3. SHOW — how many of the filtered + sorted reviews to render. Honored
444 // only by stacked, paginatable layouts (caps.load_more). Free shows up to
445 // 10; lifts to 50 once the EmbedPress API is connected.
446 $reviews_max = \EmbedPress\Includes\Classes\GoogleReviewsManaged::is_connected() ? 50 : 10;
447 $this->add_control('ep_gr_limit', [
448 'label' => __('Reviews per page', 'embedpress'),
449 'type' => Controls_Manager::NUMBER,
450 'default' => 10,
451 'min' => 1,
452 'max' => $reviews_max,
453 'description' => $reviews_max > 10
454 ? __('How many of the filtered, sorted reviews to show (the page size when “Load more” is on).', 'embedpress')
455 : __('Show up to 10 reviews per place. Connect the EmbedPress API in EmbedPress → Google Reviews to show more.', 'embedpress'),
456 'condition' => ['ep_gr_layout' => self::caps_layouts('load_more')],
457 ]);
458
459 // "Load more" pairs with "Reviews per page" — the count IS the page size.
460 // FREE (frontend pagination). Only for paginatable layouts.
461 $this->add_control('ep_gr_load_more', [
462 'label' => __('Load More', 'embedpress'),
463 'type' => Controls_Manager::SWITCHER,
464 'return_value' => 'yes',
465 'default' => 'yes',
466 'description' => __('Reveals reviews a page at a time. Page size = the “Reviews per page” count above.', 'embedpress'),
467 'condition' => ['ep_gr_layout' => self::caps_layouts('load_more')],
468 ]);
469
470 $this->end_controls_section();
471 }
472
473 /**
474 * Style / SEO / Sources — Pro controls, defined in the free widget, marked
475 * with $this->pro_class. Mirrors the Gutenberg block's locked Pro panels.
476 */
477 protected function register_pro_appearance_controls()
478 {
479 /* Motion — caps-driven, mirrors the Gutenberg "Motion" panel. One section
480 * holding the slider nav (arrows/dots/loop, free), the autoplay toggle
481 * (+ speed, Pro), and the marquee scroll-speed (free). The section shows
482 * only for layouts with at least one motion cap; each control inside is
483 * further gated to the exact layouts that use it. */
484 $motion_layouts = array_values(array_unique(array_merge(
485 self::caps_layouts('slider'),
486 self::caps_layouts('autoplay'),
487 self::caps_layouts('speed')
488 )));
489 $this->start_controls_section('ep_gr_motion', [
490 'label' => __('Motion', 'embedpress'),
491 'condition' => ['ep_gr_layout' => $motion_layouts],
492 ]);
493 // Slider navigation (free) — carousel-style arrows/dots/loop.
494 $this->add_control('ep_gr_show_arrows', [
495 'label' => __('Show arrows', 'embedpress'),
496 'type' => Controls_Manager::SWITCHER,
497 'default' => 'yes',
498 'return_value' => 'yes',
499 'condition' => ['ep_gr_layout' => self::caps_layouts('slider')],
500 ]);
501 $this->add_control('ep_gr_show_dots', [
502 'label' => __('Show dots', 'embedpress'),
503 'type' => Controls_Manager::SWITCHER,
504 'default' => 'yes',
505 'return_value' => 'yes',
506 'condition' => ['ep_gr_layout' => self::caps_layouts('slider')],
507 ]);
508 $this->add_control('ep_gr_loop', [
509 'label' => __('Infinite loop', 'embedpress'),
510 'type' => Controls_Manager::SWITCHER,
511 'default' => 'yes',
512 'return_value' => 'yes',
513 'description' => __('Slide seamlessly from the last review back to the first.', 'embedpress'),
514 'condition' => ['ep_gr_layout' => self::caps_layouts('slider')],
515 ]);
516 // Autoplay (FREE — mirrors Essential Addons) — auto-advance + speed.
517 $this->add_control('ep_gr_autoplay', [
518 'label' => __('Autoplay', 'embedpress'),
519 'type' => Controls_Manager::SWITCHER,
520 'return_value' => 'yes',
521 'condition' => ['ep_gr_layout' => self::caps_layouts('autoplay')],
522 ]);
523 $this->add_control('ep_gr_autoplay_speed', [
524 'label' => __('Autoplay speed (seconds)', 'embedpress'),
525 'type' => Controls_Manager::NUMBER,
526 'default' => 5,
527 'min' => 1,
528 'max' => 30,
529 'condition' => [
530 'ep_gr_layout' => self::caps_layouts('autoplay'),
531 'ep_gr_autoplay' => 'yes',
532 ],
533 ]);
534 // Marquee scroll speed (free) — intuitive 1–10 scale, higher = faster.
535 $this->add_control('ep_gr_scroll_speed', [
536 'label' => __('Scroll speed', 'embedpress'),
537 'type' => Controls_Manager::SLIDER,
538 'default' => ['size' => 4],
539 'range' => ['px' => ['min' => 1, 'max' => 10, 'step' => 1]],
540 'description' => __('Marquee scroll speed, from 1 (slowest) to 10 (fastest).', 'embedpress'),
541 'condition' => ['ep_gr_layout' => self::caps_layouts('speed')],
542 ]);
543 $this->end_controls_section();
544
545 /* Colors & Theme — purely cosmetic skinning (theme + accent color). FREE
546 * (mirrors Essential Addons). Placed AFTER Motion to match the Gutenberg
547 * panel order. */
548 $this->start_controls_section('ep_gr_pro_style', ['label' => __('Colors & Theme', 'embedpress')]);
549 $this->add_control('ep_gr_theme', [
550 'label' => __('Theme', 'embedpress'),
551 'type' => Controls_Manager::SELECT,
552 'default' => 'light',
553 'options' => ['light' => __('Light', 'embedpress'), 'dark' => __('Dark', 'embedpress')],
554 ]);
555 $this->add_control('ep_gr_accent_color', [
556 'label' => __('Accent color', 'embedpress'),
557 'type' => Controls_Manager::COLOR,
558 ]);
559 $this->end_controls_section();
560
561 /* SEO / Rich Snippets */
562 $this->start_controls_section('ep_gr_pro_seo', ['label' => __('SEO / Rich Snippets', 'embedpress')]);
563 // JSON-LD schema is FREE (mirrors Essential Addons' Local Business schema).
564 $this->add_control('ep_gr_schema', [
565 'label' => __('Output review schema (JSON-LD)', 'embedpress'),
566 'type' => Controls_Manager::SWITCHER,
567 'return_value' => 'yes',
568 'description' => __('Adds AggregateRating + Review structured data so search engines can show star ratings.', 'embedpress'),
569 ]);
570 $this->end_controls_section();
571
572 /* Sources & Caching */
573 $this->start_controls_section('ep_gr_pro_sources', ['label' => __('Sources & Caching', 'embedpress')]);
574
575 // Cache duration is FREE (mirrors Essential Addons' "Data Cache Time").
576 $this->add_control('ep_gr_cache_ttl', [
577 'label' => __('Cache duration (hours)', 'embedpress'),
578 'type' => Controls_Manager::NUMBER,
579 'default' => 0,
580 'min' => 0,
581 'max' => 168,
582 'description' => __('0 = use the global setting.', 'embedpress'),
583 ]);
584
585 // Multi-place merge (repeater) — PRO. Parity with the Gutenberg block.
586 $place_repeater = new \Elementor\Repeater();
587 $place_repeater->add_control('place_id', [
588 'label' => __('Place ID', 'embedpress'),
589 'type' => Controls_Manager::TEXT,
590 'placeholder' => 'ChIJ…',
591 'label_block' => true,
592 ]);
593 $this->add_control('ep_gr_places', [
594 'label' => sprintf('%1$s %2$s', __('Additional places', 'embedpress'), $this->pro_text),
595 'type' => Controls_Manager::REPEATER,
596 'classes' => $this->pro_class,
597 'fields' => $place_repeater->get_controls(),
598 'prevent_empty' => false,
599 'title_field' => '{{{ place_id }}}',
600 'description' => __('Merge reviews from more than one Google place (by Place ID).', 'embedpress'),
601 ]);
602 $this->end_controls_section();
603 }
604
605 /**
606 * (Removed) The old whole-section upsell builder. Pro controls are now
607 * defined inline in register_pro_filtering_controls() /
608 * register_pro_appearance_controls(), gated per-control by $this->pro_class
609 * the canonical EmbedPress pattern. This stub is intentionally empty and kept
610 * only so any stale external reference no-ops instead of fatal-erroring.
611 */
612 protected function register_pro_upsell_sections()
613 {
614 $groups = [];
615
616 foreach ($groups as $id => $g) {
617 $this->start_controls_section($id, ['label' => $g['label']]);
618 $this->add_control($id . '_note', [
619 'type' => Controls_Manager::RAW_HTML,
620 'raw' => '',
621 'content_classes' => 'ep-gr-elementor-upsell-wrap',
622 ]);
623 $this->end_controls_section();
624 }
625 }
626
627 protected function render()
628 {
629 $s = $this->get_settings_for_display();
630
631 GoogleReviewsRenderer::enqueue_assets();
632
633 $place = isset($s['ep_gr_place']) && is_array($s['ep_gr_place']) ? $s['ep_gr_place'] : [];
634
635 $args = [
636 'place_id' => isset($place['place_id']) ? sanitize_text_field($place['place_id']) : '',
637 'place_name' => isset($place['place_name']) ? sanitize_text_field($place['place_name']) : '',
638 'limit' => isset($s['ep_gr_limit']) ? (int) $s['ep_gr_limit'] : 10,
639 'min_rating' => isset($s['ep_gr_min_rating']) ? (int) $s['ep_gr_min_rating'] : 0,
640 'layout' => isset($s['ep_gr_layout']) ? sanitize_key($s['ep_gr_layout']) : 'list',
641 'show_photo' => ($s['ep_gr_show_photo'] ?? 'yes') === 'yes',
642 'show_stars' => ($s['ep_gr_show_stars'] ?? 'yes') === 'yes',
643 'show_date' => ($s['ep_gr_show_date'] ?? 'yes') === 'yes',
644 'show_images' => ($s['ep_gr_show_images'] ?? 'yes') === 'yes',
645 'show_link' => ($s['ep_gr_show_link'] ?? '') === 'yes',
646 // Header / summary (parity with the Gutenberg block). Default ON to
647 // match the renderer defaults, so existing widgets keep their header.
648 'show_summary' => ($s['ep_gr_show_summary'] ?? 'yes') === 'yes',
649 'show_summary_name' => ($s['ep_gr_show_summary_name'] ?? 'yes') === 'yes',
650 'show_summary_rating' => ($s['ep_gr_show_summary_rating'] ?? 'yes') === 'yes',
651 'show_summary_stars' => ($s['ep_gr_show_summary_stars'] ?? 'yes') === 'yes',
652 'show_summary_count' => ($s['ep_gr_show_summary_count'] ?? 'yes') === 'yes',
653 'summary_align' => isset($s['ep_gr_summary_align']) ? sanitize_key($s['ep_gr_summary_align']) : 'left',
654 // Columns: sliders read the "Slides per view" control, grid-type
655 // layouts read "Columns" — both feed the same render arg.
656 'columns' => self::columns_arg($s),
657 'gap' => isset($s['ep_gr_gap']) ? (int) $s['ep_gr_gap'] : 20,
658 'max_width' => isset($s['ep_gr_max_width']) ? (int) $s['ep_gr_max_width'] : 0,
659 // Motion (free): slider nav + scroll speed. Marquee scroll speed and
660 // the autoplay speed share the renderer's `autoplay_speed` arg.
661 'show_arrows' => ($s['ep_gr_show_arrows'] ?? 'yes') === 'yes',
662 'show_dots' => ($s['ep_gr_show_dots'] ?? 'yes') === 'yes',
663 'carousel_loop' => ($s['ep_gr_loop'] ?? 'yes') === 'yes',
664 // Speed: marquee uses the 1–10 "Scroll speed" SLIDER; carousel/
665 // spotlight use the "Autoplay speed (seconds)" NUMBER. Both flow
666 // through the renderer's autoplay_speed arg (each layout's JS reads it
667 // its own way), so resolve whichever applies.
668 'autoplay_speed' => self::speed_arg($s),
669 // FREE (mirrors Essential Addons): sort / keyword / hide-empty,
670 // theme / accent, autoplay, JSON-LD schema, cache duration, load-more.
671 // These map straight onto the renderer args here in the FREE widget
672 // (the free GoogleReviewsRenderer implements the behaviour). Only
673 // multi-place merge (`places`) stays Pro, mapped via the filter below.
674 'sort' => isset($s['ep_gr_sort']) ? sanitize_key($s['ep_gr_sort']) : 'newest',
675 'keyword' => isset($s['ep_gr_keyword']) ? sanitize_text_field($s['ep_gr_keyword']) : '',
676 'hide_empty' => ($s['ep_gr_hide_empty'] ?? '') === 'yes',
677 'load_more' => ($s['ep_gr_load_more'] ?? '') === 'yes',
678 'theme' => isset($s['ep_gr_theme']) ? sanitize_key($s['ep_gr_theme']) : 'light',
679 'accent_color' => isset($s['ep_gr_accent_color']) ? sanitize_hex_color($s['ep_gr_accent_color']) : '',
680 'autoplay' => ($s['ep_gr_autoplay'] ?? '') === 'yes',
681 'schema' => ($s['ep_gr_schema'] ?? '') === 'yes',
682 'cache_ttl' => isset($s['ep_gr_cache_ttl']) ? ((int) $s['ep_gr_cache_ttl']) * HOUR_IN_SECONDS : 0,
683 ];
684
685 /**
686 * Let Pro map its remaining Elementor control values onto the renderer
687 * args — now only `places` (multi-place merge). The free controls above
688 * are already mapped; Pro's elementor_render_args no longer re-maps them.
689 */
690 $args = (array) apply_filters('embedpress/google_reviews/elementor_render_args', $args, $s, $this);
691
692 echo GoogleReviewsRenderer::render($args);
693 }
694 }
695