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