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 |