PluginProbe ʕ •ᴥ•ʔ
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More / 4.0.0
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More v4.0.0
4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 4.0.2 4.0.1 4.0.0 trunk 1.0.0 2.0.0 2.0.1 2.0.2 2.0.3 3.0 3.0.1 3.0.2 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.2 3.1.3 3.2.0 3.2.1 3.2.2 3.2.4 3.2.5 3.2.7 3.2.8 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.4.5 3.4.6 3.5.0 3.5.1 3.5.2 3.5.3 3.5.4 3.5.6 3.5.7 3.5.8 3.5.9 3.6.0 3.6.1 3.6.2 3.7.0 3.7.1
superb-blocks / src / gutenberg / class-gutenberg-enhancements-controller.php
superb-blocks / src / gutenberg Last commit date
block-api 2 months ago form 2 months ago import 2 months ago popup 2 months ago templates 2 months ago class-gutenberg-block-styles.php 2 months ago class-gutenberg-cache-util.php 2 months ago class-gutenberg-conditions-controller.php 2 months ago class-gutenberg-controller.php 2 months ago class-gutenberg-dynamic-content-controller.php 2 months ago class-gutenberg-enhancements-controller.php 2 months ago class-gutenberg-social-icons-controller.php 2 months ago class-gutenberg-sticky-controller.php 2 months ago class-gutenberg-z-index-controller.php 2 months ago
class-gutenberg-enhancements-controller.php
751 lines
1 <?php
2
3 namespace SuperbAddons\Gutenberg\Controllers;
4
5 defined('ABSPATH') || exit();
6
7 use Exception;
8 use SuperbAddons\Config\Capabilities;
9 use SuperbAddons\Data\Controllers\LogController;
10 use SuperbAddons\Data\Controllers\Option;
11 use SuperbAddons\Data\Controllers\RestController;
12
13 use WP_Block_Type_Registry;
14 use WP_Error;
15 use WP_REST_Server;
16 use WP_HTML_Tag_Processor;
17
18 class GutenbergEnhancementsController
19 {
20 const ENHANCEMENTS_OPTION = 'superb-blocks-enhancements';
21
22 const HIGHLIGHTS_KEY = 'superb-blocks-highlights';
23 const HIGHLIGHTS_QUICKOPTIONS_KEY = 'superb-blocks-highlights-qo';
24 const HIGHLIGHTS_QUICKOPTIONS_BOTTOM_KEY = 'superb-blocks-highlights-qo-b';
25 const RESPONSIVE_KEY = 'superb-blocks-responsive';
26 const ANIMATIONS_KEY = 'superb-blocks-animations';
27 const CONDITIONS_KEY = 'superb-blocks-conditions';
28 const DYNAMIC_CONTENT_KEY = 'superb-blocks-dynamic-content';
29 const NAVIGATION_KEY = 'superb-blocks-navigation';
30 const RICHTEXT_KEY = 'superb-blocks-richtext';
31 const SOCIAL_ICONS_KEY = 'superb-blocks-social-icons';
32 const DASHBOARD_SHORTCUTS_KEY = 'superb-blocks-dashboard-shortcuts';
33 const STICKY_KEY = 'superb-blocks-sticky';
34 const Z_INDEX_KEY = 'superb-blocks-z-index';
35 const PANEL_DEFAULT_STATE_KEY = 'superb-blocks-panel-default-state';
36
37 private static $global_keys = array(
38 self::RESPONSIVE_KEY,
39 self::ANIMATIONS_KEY,
40 self::CONDITIONS_KEY,
41 self::DYNAMIC_CONTENT_KEY,
42 self::NAVIGATION_KEY,
43 self::RICHTEXT_KEY,
44 self::SOCIAL_ICONS_KEY,
45 self::DASHBOARD_SHORTCUTS_KEY,
46 self::STICKY_KEY,
47 self::Z_INDEX_KEY,
48 );
49
50 private static $entrance_animations = array(
51 'fadeIn',
52 'fadeInUp',
53 'fadeInDown',
54 'fadeInLeft',
55 'fadeInRight',
56 'slideInUp',
57 'slideInDown',
58 'slideInLeft',
59 'slideInRight',
60 'zoomIn',
61 'zoomInUp',
62 'zoomInDown',
63 'zoomInLeft',
64 'zoomInRight',
65 'bounceIn',
66 'bounceInUp',
67 'bounceInDown',
68 'bounceInLeft',
69 'bounceInRight',
70 'flipInX',
71 'flipInY',
72 'rotateIn',
73 'rotateInUpLeft',
74 'rotateInUpRight',
75 'rotateInDownLeft',
76 'rotateInDownRight',
77 'revealltr',
78 'revealrtl',
79 'revealbtt',
80 'revealttb'
81 );
82
83 private static $letter_animations = array(
84 'letterZoomIn',
85 'letterFadeIn',
86 'letterSlideUp',
87 'letterSpiralUp',
88 'letterBounceIn',
89 'letterFlipIn',
90 'letterSlideInRight',
91 'letterRiseUp',
92 'letterDropDown',
93 'letterFlyInRight',
94 'letterBounce',
95 'letterGentleFade'
96 );
97
98 public static function Initialize()
99 {
100 self::InitializeEnhancementEndpoints();
101 self::InitializeEditorEnhancements();
102 GutenbergBlockStyles::Initialize();
103 }
104
105 private static function InitializeEnhancementEndpoints()
106 {
107 RestController::AddRoute('/options/enhancements', array(
108 'methods' => WP_REST_Server::READABLE,
109 'permission_callback' => array(self::class, 'OptionsCallbackPermissionCheck'),
110 'callback' => array(self::class, 'OptionsCallback'),
111 ));
112 RestController::AddRoute('/options/enhancements', array(
113 'methods' => WP_REST_Server::EDITABLE,
114 'permission_callback' => array(self::class, 'OptionsCallbackPermissionCheck'),
115 'callback' => array(self::class, 'OptionsSaveCallback'),
116 ));
117 }
118
119 private static function InitializeEditorEnhancements()
120 {
121 // Use global (site-wide) settings for frontend rendering, not per-user.
122 // This ensures consistent output regardless of who is logged in.
123 $options = self::GetGlobalEnhancementsOptions();
124
125 if (isset($options[self::CONDITIONS_KEY]) && $options[self::CONDITIONS_KEY]) {
126 GutenbergConditionsController::Initialize();
127 }
128 if (isset($options[self::DYNAMIC_CONTENT_KEY]) && $options[self::DYNAMIC_CONTENT_KEY]) {
129 GutenbergDynamicContentController::Initialize();
130 }
131 if (isset($options[self::SOCIAL_ICONS_KEY]) && $options[self::SOCIAL_ICONS_KEY]) {
132 GutenbergSocialIconsController::Initialize();
133 }
134 if (isset($options[self::STICKY_KEY]) && $options[self::STICKY_KEY]) {
135 GutenbergStickyController::Initialize();
136 }
137 if (isset($options[self::Z_INDEX_KEY]) && $options[self::Z_INDEX_KEY]) {
138 GutenbergZIndexController::Initialize();
139 }
140 add_filter('render_block', [__CLASS__, 'FilterEnhancementsRender'], 10, 2);
141 if (isset($options[self::NAVIGATION_KEY]) && $options[self::NAVIGATION_KEY]) {
142 add_filter('render_block', [__CLASS__, 'NavigationEnhancementsRender'], 10, 2);
143 }
144 add_filter('wp_enqueue_scripts', [__CLASS__, 'EnhancementsEnqueue']);
145
146 add_filter('rest_pre_dispatch', array(__CLASS__, 'rest_pre_dispatch'), 10, 3);
147 }
148
149 /*
150 * This function is used to remove the attributes that are not allowed for the block during the server side rendering.
151 * Solves the issue of editor server side rendering not working if blocks have custom attributes registered during runtime.
152 */
153 public static function rest_pre_dispatch($result, $server, $request)
154 {
155 if (strpos($request->get_route(), '/wp/v2/block-renderer') === false || !isset($request['context']) || $request['context'] !== 'edit') {
156 return $result;
157 }
158
159 if (!isset($request['attributes']) || !class_exists('WP_Block_Type_Registry')) {
160 return $result;
161 }
162
163 $block_type = str_replace('/wp/v2/block-renderer/', '', $request->get_route());
164 $registry = WP_Block_Type_Registry::get_instance();
165 $block = $registry->get_registered($block_type);
166 if (!$block) {
167 return $result;
168 }
169
170 $allowed_attributes = $block->get_attributes();
171 $attributes = $request['attributes'];
172
173 foreach ($attributes as $key => $value) {
174 if (!isset($allowed_attributes[$key])) {
175 unset($attributes[$key]);
176 }
177 }
178
179 $request['attributes'] = $attributes;
180
181 return $result;
182 }
183
184 public static function FilterEnhancementsRender($block_content, $block)
185 {
186 $block_content = self::MaybeAddBlockTagModifications($block_content, $block, array(
187 'spbaddHideOnMobile' => array(
188 'class' => 'superb-addons-hide-on-mobile',
189 ),
190 'spbaddHideOnTablet' => array(
191 'class' => 'superb-addons-hide-on-tablet',
192 ),
193 'spbaddHideOnDesktop' => array(
194 'class' => 'superb-addons-hide-on-desktop',
195 )
196 ));
197
198 // Process spbaddResponsive object attribute for per-feature CSS classes + custom properties
199 if (isset($block['attrs']['spbaddResponsive']) && is_array($block['attrs']['spbaddResponsive'])) {
200 $responsive = $block['attrs']['spbaddResponsive'];
201 $classes = array();
202 $style_parts = array();
203
204 self::BuildResponsiveStyles($responsive, $classes, $style_parts);
205
206 if (!empty($classes) || !empty($style_parts)) {
207 $processor = new WP_HTML_Tag_Processor($block_content);
208 if ($processor->next_tag()) {
209 foreach ($classes as $cls) {
210 $processor->add_class($cls);
211 }
212 if (!empty($style_parts)) {
213 $styles = $processor->get_attribute("style");
214 $styles = isset($styles) && $styles !== null ? $styles : "";
215 if (!empty($styles) && substr($styles, -1) !== ";") {
216 $styles .= ";";
217 }
218 $processor->set_attribute("style", $styles . implode(" ", $style_parts));
219 }
220 $block_content = $processor->get_updated_html();
221 }
222 }
223 }
224
225 $isPopupBlock = isset($block['blockName']) && $block['blockName'] === 'superb-addons/popup';
226
227 if (isset($block['attrs']['spbaddAnimationsEnabled']) && $block['attrs']['spbaddAnimationsEnabled']) {
228 $animation_id = isset($block['attrs']['spbaddAnimationId']) ? $block['attrs']['spbaddAnimationId'] : '';
229 wp_enqueue_script('superb-addons-animations', SUPERBADDONS_ASSETS_PATH . '/js/dynamic-blocks/block-animations.js', array(), SUPERBADDONS_VERSION, true);
230 $is_letter_animation = in_array($animation_id, self::$letter_animations, true);
231
232 // Letter animations don't apply to popup blocks
233 if ($is_letter_animation && $isPopupBlock) {
234 // Skip — letter animations don't make sense on a popup dialog
235 } elseif ($is_letter_animation) {
236 // Letter animation — output letter-specific data attribute
237 $animation_modifications = array(
238 'spbaddAnimationId' => array(
239 'data-spbadd-anim-letter' => array("attribute" => "spbaddAnimationId"),
240 )
241 );
242 // All letter animations except letterBounce are entrance animations
243 if ($animation_id !== 'letterBounce') {
244 $animation_modifications['spbaddAnimationId']['data-spbadd-entrance'] = '';
245 }
246
247 $block_content = self::MaybeAddBlockTagModifications($block_content, $block, $animation_modifications);
248 } elseif ($isPopupBlock) {
249 // Popup block — add animation attributes to the dialog element (not the wrapper)
250 // so the MutationObserver in block-animations.js detects visibility changes via the overlay parent
251 $animation_attrs = array(
252 'superb-addons-animation' => $animation_id,
253 );
254 if (in_array($animation_id, self::$entrance_animations, true)) {
255 $animation_attrs['data-spbadd-entrance'] = '';
256 }
257 if (isset($block['attrs']['spbaddAnimationSpeed']) && $block['attrs']['spbaddAnimationSpeed'] != 1) {
258 $animation_attrs['data-spbadd-animation-speed'] = strval($block['attrs']['spbaddAnimationSpeed']);
259 }
260 if (isset($block['attrs']['spbaddAnimationDelay']) && $block['attrs']['spbaddAnimationDelay'] > 0) {
261 $animation_attrs['data-spbadd-animation-delay'] = strval($block['attrs']['spbaddAnimationDelay']);
262 }
263 if (isset($block['attrs']['spbaddAnimationLoop']) && $block['attrs']['spbaddAnimationLoop']) {
264 $animation_attrs['data-spbadd-animation-loop'] = 'true';
265 }
266 $block_content = self::AddPopupDialogAnimationAttributes($block_content, $animation_attrs);
267 } else {
268 // Block animation — output block-specific data attributes
269 $animation_modifications = array(
270 'spbaddAnimationId' => array(
271 'superb-addons-animation' => array("attribute" => "spbaddAnimationId"),
272 )
273 );
274
275 // Entrance animations get a data attribute that CSS uses to set opacity:0 with a safety fallback
276 if (in_array($animation_id, self::$entrance_animations, true)) {
277 $animation_modifications['spbaddAnimationId']['data-spbadd-entrance'] = '';
278 }
279
280 // Shared: speed/delay/loop output for both block and letter animations
281 if (isset($block['attrs']['spbaddAnimationSpeed']) && $block['attrs']['spbaddAnimationSpeed'] != 1) {
282 $animation_modifications['spbaddAnimationSpeed'] = array(
283 'data-spbadd-animation-speed' => array("attribute" => "spbaddAnimationSpeed"),
284 );
285 }
286 if (isset($block['attrs']['spbaddAnimationDelay']) && $block['attrs']['spbaddAnimationDelay'] > 0) {
287 $animation_modifications['spbaddAnimationDelay'] = array(
288 'data-spbadd-animation-delay' => array("attribute" => "spbaddAnimationDelay"),
289 );
290 }
291 if (isset($block['attrs']['spbaddAnimationLoop']) && $block['attrs']['spbaddAnimationLoop']) {
292 $animation_modifications['spbaddAnimationLoop'] = array(
293 'data-spbadd-animation-loop' => 'true',
294 );
295 }
296
297 $block_content = self::MaybeAddBlockTagModifications($block_content, $block, $animation_modifications);
298 }
299 }
300
301 // Typing Animation (inline RichText format - detected in content)
302 if (strpos($block_content, 'spbadd-anim-typing') !== false) {
303 wp_enqueue_script('superb-addons-typing-animations', SUPERBADDONS_ASSETS_PATH . '/js/dynamic-blocks/typing-animations.js', array(), SUPERBADDONS_VERSION, true);
304 }
305
306 // Count Animation (inline RichText format - detected in content)
307 if (strpos($block_content, 'spbadd-anim-count') !== false) {
308 wp_enqueue_script('superb-addons-count-animations', SUPERBADDONS_ASSETS_PATH . '/js/dynamic-blocks/count-animations.js', array(), SUPERBADDONS_VERSION, true);
309 }
310
311 return $block_content;
312 }
313
314 public static function NavigationEnhancementsRender($block_content, $block)
315 {
316 if (!isset($block['blockName']) || $block['blockName'] !== 'core/navigation') {
317 return $block_content;
318 }
319
320 if (
321 isset($block['attrs']['spbaddMobileMenuJustification']) &&
322 !empty($block['attrs']['spbaddMobileMenuJustification']) &&
323 $block['attrs']['spbaddMobileMenuJustification'] !== 'default'
324 ) {
325 $block_content = self::MaybeAddBlockTagModifications($block_content, $block, array(
326 'spbaddMobileMenuJustification' => array(
327 'class' => 'has-superb-addons-overlay-menu-justification',
328 'required-values' => array(
329 'left' => array(
330 'class' => 'superb-addons-overlay-menu-justification-left'
331 ),
332 'center' => array(
333 'class' => 'superb-addons-overlay-menu-justification-center'
334 ),
335 'right' => array(
336 'class' => 'superb-addons-overlay-menu-justification-right'
337 ),
338 'stretch' => array(
339 'class' => 'superb-addons-overlay-menu-justification-stretch'
340 ),
341 )
342 )
343 ));
344 }
345 if (
346 isset($block['attrs']['spbaddSubmenuLayout']) &&
347 !empty($block['attrs']['spbaddSubmenuLayout']) &&
348 $block['attrs']['spbaddSubmenuLayout'] !== 'default'
349 ) {
350 $block_content = self::MaybeAddBlockTagModifications($block_content, $block, array(
351 'spbaddSubmenuLayout' => array(
352 'required-values' => array(
353 'card' => array(
354 'class' => 'is-superb-addons-submenu-layout-card'
355 ),
356 )
357 )
358 ));
359 }
360
361
362 return $block_content;
363 }
364
365 public static function EnhancementsEnqueue()
366 {
367 wp_enqueue_style('superb-addons-enhancements', SUPERBADDONS_ASSETS_PATH . '/css/enhancements.min.css', array(), SUPERBADDONS_VERSION);
368 }
369
370 public static function MaybeAddBlockTagModifications($block_content, $block, $classes)
371 {
372 if (!is_array($classes) || empty($classes)) {
373 return $block_content;
374 }
375
376 $added_html_classes = array();
377 $added_html_styles = array();
378 $added_html_attributes = array();
379 foreach ($classes as $required_attribute => $modification) {
380 if (!isset($block['attrs'][$required_attribute]) || !$block['attrs'][$required_attribute]) {
381 continue;
382 }
383 foreach ($modification as $modification_key => $value) {
384 if ($modification_key === 'required-values') {
385 foreach ($value as $required_value => $conditional_modifications) {
386 if ($block['attrs'][$required_attribute] !== $required_value) {
387 continue;
388 }
389 foreach ($conditional_modifications as $conditional_modification_key => $conditional_value) {
390 self::AppendModificationArrays($block, $conditional_modification_key, $conditional_value, $added_html_classes, $added_html_styles, $added_html_attributes);
391 }
392 }
393 continue;
394 }
395
396 self::AppendModificationArrays($block, $modification_key, $value, $added_html_classes, $added_html_styles, $added_html_attributes);
397 }
398 }
399
400 if (empty($added_html_classes) && empty($added_html_styles) && empty($added_html_attributes)) {
401 return $block_content;
402 }
403
404 $processor = new WP_HTML_Tag_Processor($block_content);
405 if (!$processor->next_tag()) {
406 return $block_content;
407 }
408 if (!empty($added_html_attributes)) {
409 foreach ($added_html_attributes as $attribute => $value) {
410 $processor->set_attribute($attribute, $value);
411 }
412 }
413 if (!empty($added_html_styles)) {
414 $styles = $processor->get_attribute("style");
415 $styles = isset($styles) ? $styles : "";
416 if (!empty($styles) && substr($styles, -1) !== ";") {
417 $styles .= ";";
418 }
419 $processor->set_attribute("style", $styles . join(" ", $added_html_styles));
420 }
421 if (!empty($added_html_classes)) {
422 $processor->add_class(join(" ", $added_html_classes));
423 }
424 return $processor->get_updated_html();
425 }
426
427 /**
428 * Add animation attributes to the .superb-popup-dialog element instead of the block wrapper.
429 * This allows the block-animations.js MutationObserver to detect visibility changes
430 * via the overlay parent when the popup opens.
431 */
432 private static function AddPopupDialogAnimationAttributes($block_content, $animation_attrs)
433 {
434 $processor = new WP_HTML_Tag_Processor($block_content);
435 while ($processor->next_tag('div')) {
436 $class = $processor->get_attribute('class');
437 if ($class !== null && strpos($class, 'superb-popup-dialog') !== false) {
438 foreach ($animation_attrs as $attr => $val) {
439 $processor->set_attribute($attr, $val);
440 }
441 return $processor->get_updated_html();
442 }
443 }
444 return $block_content;
445 }
446
447 private static function AppendModificationArrays($block, $modification_key, $value, &$added_html_classes, &$added_html_styles, &$added_html_attributes)
448 {
449 if (is_array($value)) {
450 $key = key($value);
451 $dynamic_value = $value[$key];
452 switch ($key) {
453 case 'attribute':
454 $value = isset($block['attrs'][$dynamic_value]) ? $block['attrs'][$dynamic_value] : (isset($value['default']) ? $value['default'] : '');
455 break;
456 case 'json-attribute':
457 $raw = isset($block['attrs'][$dynamic_value]) ? $block['attrs'][$dynamic_value] : (isset($value['default']) ? $value['default'] : array());
458 $value = wp_json_encode($raw);
459 break;
460 default:
461 $value = "";
462 break;
463 }
464 }
465 switch ($modification_key) {
466 case 'class':
467 $added_html_classes[] = $value;
468 break;
469 case 'style':
470 $added_html_styles[] = $value;
471 break;
472 default:
473 $added_html_attributes[$modification_key] = $value;
474 break;
475 }
476 }
477
478 /**
479 * Map of feature keys to their CSS class and CSS var prefix.
480 * Box properties (padding, margin) use per-side vars.
481 * Simple properties use a single var.
482 */
483 private static $responsive_feature_map = array(
484 'padding' => array(
485 'class' => 'superb-addons-has-padding',
486 'type' => 'box',
487 'prefix' => '--spbadd-padding',
488 ),
489 'margin' => array(
490 'class' => 'superb-addons-has-margin',
491 'type' => 'box',
492 'prefix' => '--spbadd-margin',
493 ),
494 'fontSize' => array(
495 'class' => 'superb-addons-has-font-size',
496 'type' => 'simple',
497 'prefix' => '--spbadd-font-size',
498 ),
499 'lineHeight' => array(
500 'class' => 'superb-addons-has-line-height',
501 'type' => 'simple',
502 'prefix' => '--spbadd-line-height',
503 ),
504 'textAlign' => array(
505 'class' => 'superb-addons-has-text-align',
506 'type' => 'simple',
507 'prefix' => '--spbadd-text-align',
508 ),
509 'maxWidth' => array(
510 'class' => 'superb-addons-has-max-width',
511 'type' => 'simple',
512 'prefix' => '--spbadd-max-width',
513 ),
514 'gap' => array(
515 'class' => 'superb-addons-has-gap',
516 'type' => 'simple',
517 'prefix' => '--spbadd-gap',
518 ),
519 'flexDir' => array(
520 'class' => 'superb-addons-has-flex-dir',
521 'type' => 'simple',
522 'prefix' => '--spbadd-flex-dir',
523 ),
524 'justifyContent' => array(
525 'class' => 'superb-addons-has-justify-content',
526 'type' => 'simple',
527 'prefix' => '--spbadd-justify-content',
528 ),
529 'alignItems' => array(
530 'class' => 'superb-addons-has-align-items',
531 'type' => 'simple',
532 'prefix' => '--spbadd-align-items',
533 ),
534 'order' => array(
535 'class' => 'superb-addons-has-order',
536 'type' => 'simple',
537 'prefix' => '--spbadd-order',
538 ),
539 'colCount' => array(
540 'class' => 'superb-addons-has-col-count',
541 'type' => 'simple',
542 'prefix' => '--spbadd-col-count',
543 ),
544 );
545
546 private static $responsive_devices = array('desktop', 'tablet', 'mobile');
547 private static $responsive_box_sides = array('top', 'right', 'bottom', 'left');
548
549 /**
550 * Build per-feature CSS classes and inline CSS custom property style strings.
551 *
552 * @param array $responsive The spbaddResponsive object from block attributes.
553 * @param array &$classes Array to append CSS class strings to.
554 * @param array &$styles Array to append CSS style strings to.
555 */
556 /**
557 * Convert WP spacing preset format to CSS var.
558 * e.g. "var:preset|spacing|30" → "var(--wp--preset--spacing--30)"
559 */
560 private static function ToCssValue($value)
561 {
562 if (!is_string($value) || strpos($value, 'var:') !== 0) {
563 return $value;
564 }
565 return 'var(--wp--' . str_replace('|', '--', substr($value, 4)) . ')';
566 }
567
568 private static function BuildResponsiveStyles($responsive, &$classes, &$styles)
569 {
570 foreach (self::$responsive_feature_map as $feature_key => $config) {
571 if (!isset($responsive[$feature_key]) || !is_array($responsive[$feature_key])) {
572 continue;
573 }
574
575 $feature_data = $responsive[$feature_key];
576 $has_values = false;
577
578 if ($config['type'] === 'box') {
579 // Box properties: per-side + per-device classes and CSS vars.
580 // Each device that has a value gets its own class to avoid IACVT
581 // when only some devices are set (e.g. mobile but not desktop).
582 foreach (self::$responsive_box_sides as $side) {
583 foreach (self::$responsive_devices as $device) {
584 if (isset($feature_data[$device]) && is_array($feature_data[$device]) && isset($feature_data[$device][$side]) && $feature_data[$device][$side] !== '') {
585 $val = self::ToCssValue(sanitize_text_field($feature_data[$device][$side]));
586 $suffix = $device === 'desktop' ? '' : '-' . $device;
587 $styles[] = $config['prefix'] . '-' . $side . $suffix . ':' . $val . ';';
588 $classes[] = $config['class'] . '-' . $side . '-' . $device;
589 $has_values = true;
590 }
591 }
592 }
593 } else {
594 // Simple properties: per-device classes + CSS vars to avoid IACVT.
595 foreach (self::$responsive_devices as $device) {
596 if (!isset($feature_data[$device]) || $feature_data[$device] === '') {
597 continue;
598 }
599 $suffix = $device === 'desktop' ? '' : '-' . $device;
600 $val = self::ToCssValue(sanitize_text_field($feature_data[$device]));
601 $styles[] = $config['prefix'] . $suffix . ':' . $val . ';';
602 $classes[] = $config['class'] . '-' . $device;
603 $has_values = true;
604 }
605 }
606 }
607 }
608
609 public static function OptionsCallbackPermissionCheck()
610 {
611 // Restrict endpoint to only users who have the proper capability.
612 if (!current_user_can(Capabilities::CONTRIBUTOR)) {
613 return new WP_Error('rest_forbidden', esc_html__('Unauthorized. Please check user permissions.', "superb-blocks"), array('status' => 401));
614 }
615
616 return true;
617 }
618
619 public static function OptionsCallback()
620 {
621 try {
622 return rest_ensure_response(self::GetEnhancementsOptions(get_current_user_id()));
623 } catch (Exception $ex) {
624 LogController::HandleException($ex);
625 return new \WP_Error('internal_error_plugin', 'Internal Plugin Error', array('status' => 500));
626 }
627 }
628
629 public static function OptionsSaveCallback($request)
630 {
631 try {
632 $key = sanitize_title($request['action']);
633 $is_global = self::IsGlobalKey($key);
634
635 // Global settings require admin capability
636 if ($is_global && !current_user_can(Capabilities::ADMIN)) {
637 return new \WP_Error('rest_forbidden', esc_html__('Unauthorized. Only administrators can change site-wide enhancement settings.', "superb-blocks"), array('status' => 403));
638 }
639
640 switch ($key) {
641 // Per-user boolean settings
642 case self::HIGHLIGHTS_KEY:
643 case self::HIGHLIGHTS_QUICKOPTIONS_KEY:
644 case self::HIGHLIGHTS_QUICKOPTIONS_BOTTOM_KEY:
645 $userOptions = self::GetUserOnlyOptions(get_current_user_id());
646 $userOptions[$key] = isset($request['value']) && $request['value'] === 'true';
647 if (update_user_meta(get_current_user_id(), self::ENHANCEMENTS_OPTION, $userOptions) === false) {
648 throw new Exception('Failed to update user meta');
649 }
650 break;
651 // Per-user string setting
652 case self::PANEL_DEFAULT_STATE_KEY:
653 $allowed = array('open', 'closed', 'dynamic');
654 $value = isset($request['value']) ? sanitize_text_field($request['value']) : '';
655 if (!in_array($value, $allowed, true)) {
656 return new \WP_Error('invalid_request', 'Invalid value', array('status' => 400));
657 }
658 $userOptions = self::GetUserOnlyOptions(get_current_user_id());
659 $userOptions[$key] = $value;
660 if (update_user_meta(get_current_user_id(), self::ENHANCEMENTS_OPTION, $userOptions) === false) {
661 throw new Exception('Failed to update user meta');
662 }
663 break;
664 // Global boolean settings
665 case self::RESPONSIVE_KEY:
666 case self::ANIMATIONS_KEY:
667 case self::CONDITIONS_KEY:
668 case self::DYNAMIC_CONTENT_KEY:
669 case self::NAVIGATION_KEY:
670 case self::RICHTEXT_KEY:
671 case self::SOCIAL_ICONS_KEY:
672 case self::DASHBOARD_SHORTCUTS_KEY:
673 case self::STICKY_KEY:
674 case self::Z_INDEX_KEY:
675 $value = isset($request['value']) && $request['value'] === 'true';
676 if (self::SaveGlobalEnhancementOption($key, $value) === false) {
677 throw new Exception('Failed to update global enhancement option');
678 }
679 break;
680 default:
681 return new \WP_Error('invalid_request', 'Invalid Request', array('status' => 400));
682 }
683
684 return rest_ensure_response(array('success' => true));
685 } catch (Exception $ex) {
686 LogController::HandleException($ex);
687 return new \WP_Error('internal_error_plugin', 'Internal Plugin Error', array('status' => 500));
688 }
689 }
690
691 public static function IsGlobalKey($key)
692 {
693 return in_array($key, self::$global_keys, true);
694 }
695
696 public static function GetGlobalEnhancementsOptions()
697 {
698 $defaults = array(
699 self::RESPONSIVE_KEY => true,
700 self::ANIMATIONS_KEY => true,
701 self::CONDITIONS_KEY => true,
702 self::DYNAMIC_CONTENT_KEY => true,
703 self::NAVIGATION_KEY => true,
704 self::RICHTEXT_KEY => true,
705 self::SOCIAL_ICONS_KEY => true,
706 self::DASHBOARD_SHORTCUTS_KEY => true,
707 self::STICKY_KEY => true,
708 self::Z_INDEX_KEY => true,
709 );
710 $options = get_option(Option::GLOBAL_ENHANCEMENTS, array());
711 if (!is_array($options)) {
712 return $defaults;
713 }
714 return wp_parse_args($options, $defaults);
715 }
716
717 private static function SaveGlobalEnhancementOption($key, $value)
718 {
719 $options = self::GetGlobalEnhancementsOptions();
720 $options[$key] = $value;
721 return update_option(Option::GLOBAL_ENHANCEMENTS, $options);
722 }
723
724 private static function GetUserOnlyOptions($user_id)
725 {
726 $defaults = array(
727 self::HIGHLIGHTS_KEY => true,
728 self::HIGHLIGHTS_QUICKOPTIONS_KEY => false,
729 self::HIGHLIGHTS_QUICKOPTIONS_BOTTOM_KEY => false,
730 self::PANEL_DEFAULT_STATE_KEY => 'open',
731 );
732 $options = get_user_meta($user_id, self::ENHANCEMENTS_OPTION, true);
733 if (!is_array($options)) {
734 return $defaults;
735 }
736 // Only return per-user keys, ignore any stale global keys in user_meta
737 $result = array();
738 foreach ($defaults as $k => $default_val) {
739 $result[$k] = isset($options[$k]) ? $options[$k] : $default_val;
740 }
741 return $result;
742 }
743
744 public static function GetEnhancementsOptions($user_id)
745 {
746 $global = self::GetGlobalEnhancementsOptions();
747 $per_user = self::GetUserOnlyOptions($user_id);
748 return array_merge($global, $per_user);
749 }
750 }
751