PluginProbe ʕ •ᴥ•ʔ
Kirki – Freeform Page Builder, Website Builder & Customizer / trunk
Kirki – Freeform Page Builder, Website Builder & Customizer vtrunk
6.2.5 6.2.4 6.2.3 6.2.2 6.2.1 6.2.0 6.1.1 6.1.0 6.0.14 6.0.13 6.0.12 6.0.11 6.0.10 6.0.9 6.0.8 6.0.7 6.0.6 6.0.5 6.0.4 6.0.3 6.0.2 6.0.1 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.1.8 3.1.9 4.0.19 4.0.20 4.0.21 4.0.22 4.0.23 4.0.24 4.1 4.2.0 5.0.0 5.1.0 5.1.1 5.2.0 5.2.1 5.2.2 5.2.3 6.0.0 trunk 3.0.40 3.0.41 3.0.42 3.0.43 3.0.44 3.0.45 3.1.0 3.1.1 3.1.2
kirki / includes / Frontend / Preview / Preview.php
kirki / includes / Frontend / Preview Last commit date
templates 4 months ago DataHelper.php 3 weeks ago ExceptionalElements.php 2 weeks ago Preview.php 1 week ago Utils.php 2 weeks ago
Preview.php
2495 lines
1 <?php
2 /**
3 * Preview script for html markup generator
4 *
5 * @package kirki
6 */
7
8 namespace Kirki\Frontend\Preview;
9
10 if ( ! defined( 'ABSPATH' ) ) {
11 exit; // Exit if accessed directly.
12 }
13
14 use Kirki\Ajax\Symbol;
15 use Kirki\Frontend\Preview\ExceptionalElements;
16 use Kirki\Ajax\UserData;
17 use Kirki\Ajax\WpAdmin;
18 use Kirki\API\ContentManager\ContentManagerHelper;
19 use Kirki\HelperFunctions;
20
21 use function Kirki\Framework\dd;
22
23 /**
24 * Preview class
25 */
26 class Preview extends ExceptionalElements {
27
28 /**
29 * $root for entry id or parent element id.
30 */
31 protected $root = null;
32 /**
33 * $data is for all element data array.
34 */
35 protected $data = array();
36 /**
37 * $symbol_id if preview is generate for symbol then it has symbol_id for maintain class-prefix.
38 */
39 protected $symbol_id = null;
40 protected $prefix = false;
41 /**
42 * $style_blocks for all style blocks merged array. like=> global, migrated, symbols. etc.
43 */
44 protected $style_blocks = array();
45 /**
46 * $only_used_style_blocks only used style blocks.
47 */
48 private $only_used_style_blocks = array();
49 /**
50 * $only_used_popup_id_array only used popup post ids.
51 */
52 public static $only_used_popup_id_array = array();
53 /**
54 * $printed_font_family_tracker for tracking already printed font family markup.
55 */
56 private static $printed_font_family_tracker = array();
57 private static $printed_variable_tracker = array();
58 private static $section_id_tracker_for_href = array(); // ['section_id' => 'Section-2', 'section_id_1' => 'section-2-1' ]
59 /**
60 * $view_ports user all viewports data.
61 */
62 private $view_ports = array();
63
64 /**
65 * Initialize some variables for element related data. Like: custom codes, lightbox, map, slider etc.
66 * START
67 */
68 /**
69 * $sliders for collect slider elements data.
70 */
71 private $sliders = array();
72 /**
73 * $navigation for collect navigation elements data.
74 */
75 private $navigations = array();
76
77 /**
78 * $navigation item for collect navigation item elements data.
79 */
80 private $navigation_item = array();
81
82
83 /**
84 * $inputs for collect inputs elements data.
85 */
86 private $inputs = array();
87 /**
88 * $maps for collect map elements data.
89 */
90 private $maps = array();
91 /**
92 * $lotties for collect lottie elements data.
93 */
94 private $lotties = array();
95 /**
96 * $popups for collect popup elements data.
97 */
98 private $popups = array();
99 /**
100 * $tabs for collect tab elements data.
101 */
102 private $tabs = array();
103 /**
104 * $lightboxes for collect lightbox elements data.
105 */
106 private $lightboxes = array();
107 /**
108 * $re_captchas for collect reCaptcha elements data.
109 */
110 private $re_captchas = array();
111 /**
112 * $videos for collect video elements data.
113 */
114 private $videos = array();
115 /**
116 * $interactions for collect interaction elements data.
117 */
118 private $interactions = array();
119
120 /**
121 * $interaction library for collect interaction library elements data.
122 */
123 private $interaction_library = array();
124 /**
125 * $collections for collect collection elements data.
126 */
127 private $collections = array();
128 /**
129 * $forms for collect form elements data.
130 */
131 private $forms = array();
132
133 /**
134 * $liquid_glass for liquid glass effect
135 */
136 private $liquid_glass = array();
137
138 /**
139 * $custom_codes for collect customCode elements data.
140 */
141 public $custom_codes = '';
142
143 /**
144 * $dropdown for collect dropdown elements data.
145 */
146 private $dropdown = array();
147 /**
148 * Initialize some variables for element related data. Like: custom codes, lightbox, map, slider etc.
149 * END
150 */
151
152 /**
153 * elements wise variable modes array.
154 */
155 private $ele_variable_modes = array();
156
157
158 /**
159 * interaction ['scroll-into-ele'] & ['scroll-out-ele'] tracker
160 */
161 private $interaction_preset_and_text_animation_tracker = array();
162
163 private $interaction_library_text_animation_tracker = array();
164
165 private $scroll_into_custom_interaction_tracker = '';
166
167 private $track_animation_for_elements_with_this_class = '';
168
169 private $track_animation_for_children_with_this_class = '';
170
171 private $track_animation_for_sibling_with_this_class = '';
172
173 private $track_animation_for_trigger_sibling_with_this_class = '';
174
175 private $track_animation_for_trigger_with_this_class = '';
176
177 /**
178 * END
179 */
180
181
182 /**
183 * List of exceptional elements.
184 */
185 private $exceptional_elements = array(
186 'custom-code',
187 'map',
188 'svg',
189 'svg-icon',
190 'textarea',
191 'select',
192 'video',
193 'radio-group',
194 'checkbox-element',
195 'radio-button',
196 'image',
197 'collection',
198 'loading', // collection loading element
199 'items',
200 'collection-wrapper',
201 'users',
202 'pagination',
203 'pagination-item',
204 'pagination-number',
205 'terms',
206 'menus',
207 'symbol',
208 'link-block',
209 'form',
210 'button',
211 'popup-body',
212 'navigation',
213 'navigation-item',
214 'navigation-items',
215 'section',
216 'common',
217 'slider',
218 'slider_mask',
219 'slider_nav',
220 'tabs',
221 'tab_menu',
222 'tab',
223 'tab_content',
224 'tab_pane'
225 );
226
227 /**
228 * List of dynamic content element.
229 */
230 private $exceptional_elements_contains_dyn_content = array( 'collection' );
231 /**
232 * List of inline elements.
233 */
234 private $inline_elements = array( 'button', 'link-block', 'link-text' );
235
236 /**
237 * List of anchor elements.
238 */
239 private $prevent_anchor_elements = array( 'heading', 'image', 'paragraph' );
240 /**
241 * List anchor attrs.
242 */
243 private $anchor_attrs = array( 'href', 'target', 'rel' );
244
245
246 /**
247 * Preview script initilizer
248 *
249 * @param array $data elments data array.
250 * @param array $style_blocks elments style_blocks array.
251 * @param string $root root element id.
252 * @param string $symbol_id if preview script is initiate for symbol element generation.
253 *
254 * @return void
255 */
256 public function __construct( $data, $style_blocks, $root = null, $symbol_id = null, $prefix = false ) {
257 if ( $root ) {
258 $this->root = $root;
259 } else {
260 $this->root = 'root';
261 }
262 $this->data = $data;
263 $this->style_blocks = $style_blocks;
264 $this->view_ports = UserData::get_view_port_list();
265 if ( $symbol_id ) {
266 $this->symbol_id = $symbol_id;
267 }
268 $this->prefix = $prefix;
269 }
270
271 /**
272 * Get the html string
273 *
274 * @return string
275 */
276 public function getHTML( $options = array() ) {
277 // TODO: need to fix this code
278 // if(!isset($options['user']) && get_current_user_id() > 0){
279 // $options['user'] = Users::get_user_by_id(get_current_user_id());
280 // }
281 return $this->recGenHTML( $this->root, $options );
282 }
283
284 /**
285 * Get the style blocks that are used in the html
286 * this method is called after getHtml() method call and from the same instance
287 *
288 * @return string
289 */
290 public function get_only_used_style_blocks() {
291 return $this->only_used_style_blocks;
292 }
293
294 public function add_to_only_used_style_blocks( $style_block ) {
295 $this->only_used_style_blocks[ $style_block['id'] ] = $style_block;
296 if ( isset( $style_block['liquid-glass'] ) ) {
297 $this->liquid_glass[ $style_block['id'] ] = array(
298 'id' => $style_block['id'],
299 'name' => $style_block['name'],
300 'liquid-glass' => $style_block['liquid-glass'],
301 'type' => $style_block['type'],
302 'prefix' => $this->prefix,
303 );
304 }
305 }
306
307
308 private function updateTranslateCSSString( $end ) {
309 $transforms = array();
310
311 if ( ! empty( $end['x']['unit'] ) && $end['x']['unit'] !== 'auto' ) {
312 $transforms[] = 'translateX(' . $this->addUnit( $end['x'] ) . ')';
313 }
314 if ( ! empty( $end['y']['unit'] ) && $end['y']['unit'] !== 'auto' ) {
315 $transforms[] = 'translateY(' . $this->addUnit( $end['y'] ) . ')';
316 }
317 if ( ! empty( $end['z']['unit'] ) && $end['z']['unit'] !== 'auto' ) {
318 $transforms[] = 'translateZ(' . $this->addUnit( $end['z'] ) . ')';
319 }
320
321 return implode( ' ', $transforms ); // Return only the transform part
322 }
323
324
325 private function updateRotateCSSString( $end ) {
326 $rotateProperties = array();
327
328 if ( $end['x']['unit'] !== 'auto' ) {
329 $rotateProperties[] = 'rotateX(' . $this->addUnit( $end['x'] ) . ')';
330 }
331 if ( $end['y']['unit'] !== 'auto' ) {
332 $rotateProperties[] = 'rotateY(' . $this->addUnit( $end['y'] ) . ')';
333 }
334 if ( $end['z']['unit'] !== 'auto' ) {
335 $rotateProperties[] = 'rotateZ(' . $this->addUnit( $end['z'] ) . ')';
336 }
337
338 return implode( ' ', $rotateProperties );
339 }
340
341
342 private function updateSkewCSSString( $end ) {
343 $skewProperties = array();
344
345 if ( $end['x']['unit'] !== 'auto' ) {
346 $skewProperties[] = 'skewX(' . $this->addUnit( $end['x'] ) . ')';
347 }
348 if ( $end['y']['unit'] !== 'auto' ) {
349 $skewProperties[] = 'skewY(' . $this->addUnit( $end['y'] ) . ')';
350 }
351
352 return implode( ' ', $skewProperties );
353 }
354
355
356 private function updateScaleCSSString( $end ) {
357 $scaleProperties = array();
358
359 if ( $end['x']['unit'] !== 'none' ) {
360 $scaleProperties[] = 'scaleX(' . $end['x']['value'] . ')';
361 }
362 if ( $end['y']['unit'] !== 'none' ) {
363 $scaleProperties[] = 'scaleY(' . $end['y']['value'] . ')';
364 }
365
366 return implode( ' ', $scaleProperties );
367 }
368
369
370
371 private function addUnit( $valueObj ) {
372 $specialValues = array( 'auto', 'fit-content', 'min-content', 'max-content', 'none', 'scrollHeight', 'scrollWidth' );
373
374 // Check if the unit is in specialValues, if so, don't append it
375 $unit = in_array( $valueObj['unit'], $specialValues ) ? '' : $valueObj['unit'];
376
377 return $valueObj['value'] . $unit;
378 }
379
380 private function updateCssStringForAnimation( $property, $end ) {
381 $css = '';
382 $transformProperties = array(); // Store all transform values
383
384 switch ( $property ) {
385 case 'move':
386 $transformProperties[] = $this->updateTranslateCSSString( $end );
387 break;
388 case 'rotate':
389 $transformProperties[] = $this->updateRotateCSSString( $end );
390 break;
391 case 'scale':
392 $transformProperties[] = $this->updateScaleCSSString( $end );
393 break;
394 case 'size':
395 $css .= 'width: ' . ( $end['width']['value'] === 'scrollWidth' ? 'auto' : $this->addUnit( $end['width'] ) ) . ';';
396 $css .= 'height: ' . ( $end['height']['value'] === 'scrollHeight' ? 'auto' : $this->addUnit( $end['height'] ) ) . ';';
397 break;
398 case 'skew':
399 $transformProperties[] = $this->updateSkewCSSString( $end );
400 break;
401 case 'fade':
402 $css .= 'opacity: ' . $end['value'] . ';';
403 break;
404 case 'color':
405 $css .= 'color: ' . $end['value'] . '; fill: ' . $end['value'] . ';';
406 break;
407 case 'border-color':
408 $css .= 'border-color: ' . $end['value'] . ';';
409 break;
410 case 'border-radius':
411 $css .= 'border-top-left-radius: ' . $this->addUnit( $end['border-top-left-radius'] ) . ';';
412 $css .= 'border-top-right-radius: ' . $this->addUnit( $end['border-top-right-radius'] ) . ';';
413 $css .= 'border-bottom-right-radius: ' . $this->addUnit( $end['border-bottom-right-radius'] ) . ';';
414 $css .= 'border-bottom-left-radius: ' . $this->addUnit( $end['border-bottom-left-radius'] ) . ';';
415 break;
416 case 'background-color':
417 $css .= 'background: ' . $end['value'] . ';';
418 break;
419 case 'background-size-position':
420 $css .= 'background-size: ' . $this->addUnit( $end['sizeWidth'] ) . ' ' . $this->addUnit( $end['sizeHeight'] ) . ';';
421 $css .= 'background-position-x: ' . $this->addUnit( $end['positionX'] ) . ';';
422 $css .= 'background-position-y: ' . $this->addUnit( $end['positionY'] ) . ';';
423 break;
424 case 'background-size':
425 $css .= 'background-size: ' . $this->addUnit( $end['sizeWidth'] ) . ' ' . $this->addUnit( $end['sizeHeight'] ) . ';';
426 break;
427 case 'background-position':
428 $css .= 'background-position-x: ' . $this->addUnit( $end['positionX'] ) . ';';
429 $css .= 'background-position-y: ' . $this->addUnit( $end['positionY'] ) . ';';
430 break;
431 case 'filter':
432 $filters = array();
433 foreach ( $end as $key => $value ) {
434 $filters[] = "$key(" . $this->addUnit( $value ) . ')';
435 }
436 $css .= 'filter: ' . implode( ' ', $filters ) . ';';
437 break;
438
439 default:
440 break;
441 }
442
443 if ( ! empty( $transformProperties ) ) {
444 $css .= implode( ' ', array_unique( $transformProperties ) );
445 $transformProperties = array(); // Reset for the next element
446 }
447
448 return $css;
449 }
450
451
452 /**
453 * Get the style tag string
454 *
455 * @param array|bool $blocks all styleblocks.
456 * @return string
457 */
458 public function getStyleTag( $blocks = false ) {
459 if ( ! $blocks ) {
460 $blocks = $this->style_blocks;
461 }
462
463 $s = '';
464
465 if ( count( $this->ele_variable_modes ) > 0 ) {
466 $s .= $this->getElementWiseVariableCssCodes();
467 }
468
469 if ( $blocks ) {
470 foreach ( $this->view_ports as $key => $vp ) {
471 $css = '';
472 foreach ( $blocks as $style_block ) {
473 $css .= $this->getRawCssDeviceWise( $style_block, $vp );
474 }
475 if ( $css ) {
476 $s .= "<style data='" .'kirki-element-styles-' . $key . "'>";
477 $s .= $css;
478 $s .= '</style>';
479 }
480 }
481 }
482 return $s;
483 }
484
485 public function get_interaction_set_as_initial_css() {
486 $s = '';
487 if ( count( $this->interaction_preset_and_text_animation_tracker ) > 0 || count( $this->interaction_library_text_animation_tracker ) > 0 ) {
488 $animation_css = '';
489 foreach ( $this->interaction_preset_and_text_animation_tracker as $ele_id => $bool ) {
490 $animation_css .= "[data-kirki='" . $ele_id . "'] { visibility: hidden; }";
491 }
492
493 foreach ( $this->interaction_library_text_animation_tracker as $ele_id => $devices ) {
494 $css_value = "[data-kirki='" . $ele_id . "'] { visibility: hidden; }";
495 $media_queries = ! empty( $devices ) ? $this->setMediaQuery( $css_value, $devices ) : '';
496 $animation_css .= ! empty( $media_queries ) ? $media_queries : $css_value;
497 }
498
499 if ( $animation_css ) {
500 $s .= "<style data='kirki-element-animation-visibility'>";
501 $s .= $animation_css;
502 $s .= '</style>';
503 }
504 }
505
506 if ( $this->scroll_into_custom_interaction_tracker ) {
507 $s .= "<style data='kirki-scroll-into-custom-animation'>";
508 $s .= $this->scroll_into_custom_interaction_tracker;
509 $s .= '</style>';
510 }
511
512 if ( $this->track_animation_for_elements_with_this_class ) {
513 $s .= "<style data='kirki-custom-animation-for-all-elements-with-same-class'>";
514 $s .= $this->track_animation_for_elements_with_this_class;
515 $s .= '</style>';
516 }
517
518 if ( $this->track_animation_for_children_with_this_class ) {
519 $s .= "<style data='kirki-custom-animation-for-all-children-with-same-class'>";
520 $s .= $this->track_animation_for_children_with_this_class;
521 $s .= '</style>';
522 }
523
524 if ( $this->track_animation_for_sibling_with_this_class ) {
525 $s .= "<style data='kirki-custom-animation-for-all-sibling-with-same-class'>";
526 $s .= $this->track_animation_for_sibling_with_this_class;
527 $s .= '</style>';
528 }
529
530 if ( $this->track_animation_for_trigger_sibling_with_this_class ) {
531 $s .= "<style data='kirki-custom-animation-for-trigger-sibling-with-same-class'>";
532 $s .= $this->track_animation_for_trigger_sibling_with_this_class;
533 $s .= '</style>';
534 }
535
536 if ( $this->track_animation_for_trigger_with_this_class ) {
537 $s .= "<style data='kirki-custom-animation-for-trigger-with-same-class'>";
538 $s .= $this->track_animation_for_trigger_with_this_class;
539 $s .= '</style>';
540 }
541
542 return $s;
543 }
544
545 private function setInitialAnimation( $ele_id, $animations, $single_res_value ) {
546 $elementStyleValues = array();
547 $hasClassName = false;
548 $css = '';
549 $media_queries = '';
550
551 foreach ( $animations as $ani_key => $animation ) {
552 if ( ! empty( $animation['setAsInitial'] ) && $animation['setAsInitial'] ) {
553
554 foreach ( $this->processElementStyles( $ele_id, $animation ) as $key => $value ) {
555 if ( ! isset( $elementStyleValues[ $key ] ) ) {
556 $elementStyleValues[ $key ] = array(
557 'transform' => $value['transform'] ?? array(),
558 'other' => $value['other'] ?? array(),
559 );
560 } else {
561 $elementStyleValues[ $key ]['transform'] = array_merge(
562 $elementStyleValues[ $key ]['transform'] ?? array(),
563 $value['transform'] ?? array()
564 );
565 $elementStyleValues[ $key ]['other'] = array_merge(
566 $elementStyleValues[ $key ]['other'] ?? array(),
567 $value['other'] ?? array()
568 );
569 }
570 }
571 }
572 }
573
574 if ( ! empty( $elementStyleValues ) ) {
575 $css .= $this->setAnimationStyles( $elementStyleValues, $hasClassName );
576 $media_queries = $this->getMediaQuery( $css, $single_res_value );
577 $this->scroll_into_custom_interaction_tracker .= ! empty( $media_queries ) ? $media_queries : $css;
578 }
579 }
580
581
582
583
584
585 private function setAnimationStyles( $elementStyleValues, $hasClassName ) {
586 $s = '';
587 foreach ( $elementStyleValues as $ele_id => $styles ) {
588 if ( empty( $styles['transform'] ) && empty( $styles['other'] ) ) {
589 continue;
590 }
591
592 $css = $hasClassName ? '' : "[data-kirki='$ele_id'] { ";
593 $css .= ! empty( $styles['transform'] ) ? 'transform: ' . implode( ' ', $styles['transform'] ) . '; ' : '';
594 $css .= ! empty( $styles['other'] ) ? implode( ' ', $styles['other'] ) : '';
595 $css .= $hasClassName ? '' : ' }';
596
597 $s .= $css;
598 }
599 return $s;
600 }
601
602 private function processElementStyles( $ele_id, $animation ) {
603 $elementStyles = array(); // Ensure the array is initialized properly
604
605 if ( ! isset( $elementStyles[ $ele_id ] ) ) {
606 $elementStyles[ $ele_id ] = array(
607 'transform' => array(),
608 'other' => array(),
609 );
610 }
611
612 if ( ! isset( $animation['property'] ) || ! isset( $animation['end'] ) ) {
613 return array(); // Skip invalid animations
614 }
615
616 $cssValue = $this->updateCssStringForAnimation( $animation['property'], $animation['end'] );
617 if ( ! empty( $cssValue ) ) {
618
619 switch ( $animation['property'] ) {
620 case 'move':
621 case 'rotate':
622 case 'scale':
623 case 'skew':
624 $elementStyles[ $ele_id ]['transform'][] = $cssValue;
625 break;
626 default:
627 $elementStyles[ $ele_id ]['other'][] = $cssValue;
628 break;
629 }
630 }
631 return $elementStyles;
632 }
633
634
635 /**
636 * Get the html string
637 *
638 * @param array $block single style block.
639 * @param array $vp viewport.
640 *
641 * @return string
642 */
643 private function getRawCssDeviceWise( $block, $vp ) {
644 $css_string = '';
645 $selector = $this->getSelectorFromBlock( $block );
646 $variants = $block['variant'];
647
648 $global_css = "";
649 $local_css = "";
650
651 foreach ( $variants as $key => $value ) {
652 $variant = explode( '_', $key );
653 if ( $variant[0] === $vp['id'] ) {
654 if( $block && isset($block['isGlobalStyle']) && $block['isGlobalStyle'] === true){
655 $global_css .= $this->createMediaQueryString( $selector, $key, $value, $vp );
656 }else{
657 $local_css .= $this->createMediaQueryString( $selector, $key, $value, $vp );
658 }
659 }
660 }
661 return $global_css . $local_css;
662 }
663
664 /**
665 * Create Media Query String
666 *
667 * @param string $selector calss/tag selector like => .class-name.
668 * @param string $device_name media query key like => md.
669 * @param string $css_text css text like => color:red.
670 * @param object $vp current viewport object.
671 *
672 * @return string
673 */
674 private function createMediaQueryString( $selector, $device_name, $css_text, $vp ) {
675 if ( $css_text === '' ) {
676 return '';
677 }
678 $css_string = '';
679 $variant = explode( '_', $device_name );
680 $type = $vp['type'];
681 if ( count( $variant ) === 1 ) {
682 // default class or sub class for devices.
683 if ( $variant[0] === 'md' ) {
684 $css_string .= $selector . '{' . $css_text . '}';
685 } elseif ( isset( $this->view_ports[ $variant[0] ] ) ) {
686 $css_string .= '@media only screen and (' . $type . '-width: ' . $this->view_ports[ $variant[0] ][ $type . 'Width' ] . 'px) {' . $selector . '{' . $css_text . '}}';
687 }
688 } elseif ( count( $variant ) === 2 ) {
689 // active, hover, focus or pseudo class.
690 $psuedo_class = $this->checkIfHasPsuedoClassValue( $variant[1] );
691 if ( $variant[0] === 'md' ) {
692 $css_string .= $selector . $psuedo_class . '{' . $css_text . '}';
693 } elseif ( isset( $this->view_ports[ $variant[0] ] ) ) {
694 $css_string .= '@media only screen and (' . $type . '-width: ' . $this->view_ports[ $variant[0] ][ $type . 'Width' ] . 'px) {';
695 $css_string .= $selector . $psuedo_class . '{' . $css_text . '}';
696 $css_string .= '}';
697 }
698 } elseif ( count( $variant ) === 3 ) {
699 // active, hover, focus + pseudo class.
700 $psuedo_class1 = $this->checkIfHasPsuedoClassValue( $variant[1] );
701 $psuedo_class2 = $this->checkIfHasPsuedoClassValue( $variant[2] );
702
703 if ( $variant[0] === 'md' ) {
704 $css_string .= $selector . $psuedo_class1 . $psuedo_class2 . '{' . $css_text . '}';
705 } elseif ( isset( $this->view_ports[ $variant[0] ] ) ) {
706 $css_string .= '@media only screen and (' . $type . '-width: ' . $this->view_ports[ $variant[0] ][ $type . 'Width' ] . 'px) {';
707 $css_string .= $selector . $psuedo_class1 . $psuedo_class2 . '{' . $css_text . '}';
708 $css_string .= '}';
709 }
710 }
711 return $css_string;
712 }
713
714 /**
715 * Get the custom fonts links
716 *
717 * @return string
718 */
719 public function getCustomFontsLinks() {
720 $post_id = $this->symbol_id ? $this->symbol_id : HelperFunctions::get_post_id_if_possible_from_url();
721 $post = get_post( $post_id );
722
723 if ( ! $post ) {
724 return '';
725 }
726
727 $s = '';
728 if ( 'kirki_symbol' === $post->post_type ) {
729 $symbol = Symbol::get_single_symbol( $post_id, true );
730 if ( isset( $symbol['symbolData'], $symbol['symbolData']['customFonts'] ) ) {
731 foreach ( $symbol['symbolData']['customFonts'] as $key => $f ) {
732 $s .= HelperFunctions::getFontsHTMLMarkup( $f );
733 }
734 }
735 }
736 return $s;
737 }
738
739 /**
740 * Get the script tag string
741 *
742 * @return string
743 */
744 public function getScriptTag( $should_take_app_script = true ) {
745 $s = '';
746 $empty_vars = $this->getVariableString( 'Sliders', $this->sliders );
747 $empty_vars .= $this->getVariableString( 'Maps', $this->maps );
748 $empty_vars .= $this->getVariableString( 'Lotties', $this->lotties );
749 $empty_vars .= $this->getVariableString( 'Popups', $this->popups );
750 $empty_vars .= $this->getVariableString( 'Lightboxes', $this->lightboxes );
751 $empty_vars .= $this->getVariableString( 'ReCaptchas', $this->re_captchas );
752 $empty_vars .= $this->getVariableString( 'Videos', $this->videos );
753 $empty_vars .= $this->getVariableString( 'Tabs', $this->tabs );
754 $empty_vars .= $this->getVariableString( 'Interactions', $this->interactions );
755 $empty_vars .= $this->getVariableString( 'InteractionLibrary', $this->interaction_library );
756 $empty_vars .= $this->getVariableString( 'Collections', $this->collections );
757 $empty_vars .= $this->getVariableString( 'Forms', $this->forms );
758 $empty_vars .= $this->getVariableString( 'Dropdown', $this->dropdown );
759 $empty_vars .= $this->getVariableString( 'Navigations', $this->navigations );
760 $empty_vars .= $this->getVariableString( 'NavigationItem', $this->navigation_item );
761 $empty_vars .= $this->getVariableString( 'Inputs', $this->inputs );
762 $empty_vars .= $this->getVariableString( 'LiquidGlass', $this->liquid_glass );
763
764 if ( $empty_vars ) {
765 $s .= "<script data='kirki-elements-property-vars'>";
766 $s .= $empty_vars;
767 $s .= '</script>';
768 }
769
770 if ( $this->custom_codes ) {
771 $s .= "<script data='kirki-elements-property-dev-mode'>";
772 $s .= $this->custom_codes;
773 $s .= '</script>';
774 }
775
776 $updatedScriptTags = false;
777 if ( $should_take_app_script ) {
778 $updatedScriptTags = apply_filters('kirki_add_script_tags', $s );
779 }
780
781 if ( $updatedScriptTags ) {
782 $s = $updatedScriptTags;
783 }
784
785 return $s;
786 }
787
788 private function getVariableString( $name, $value ) {
789 if ( count( $value ) === 0 ) {
790 return '';
791 }
792 $prefix = 'kirki';
793 $value = wp_json_encode( $value );
794 // SECURITY (prototype-pollution guard): only merge a PRE-EXISTING value when it is
795 // an OWN property of window. `window.kirkiX === undefined` is false when
796 // `Object.prototype.kirkiX` has been polluted (crafted query params / third-party
797 // gadget), and `{...kirkiX, ...}` would then launder attacker-controlled keys into a
798 // real own property that downstream scripts trust. Starting from `{}` unless window
799 // truly owns the value keeps inherited (attacker) data out.
800 $s = "var $prefix$name = Object.assign({}, (Object.prototype.hasOwnProperty.call(window, '$prefix$name') && window.$prefix$name) || {}, $value);";
801 return $s;
802 }
803
804 /**
805 * Returns the JS code with `<script>` tag user put into `Page settings`
806 *
807 * @return string
808 */
809 public static function getBodyCustomCode() {
810 $post_id = HelperFunctions::get_post_id_if_possible_from_url();
811 $custom_code = get_post_meta( $post_id, KIRKI_PAGE_CUSTOM_CODE, true );
812 $code = "<div data-kirki-code='custom-code'>";
813
814 if ( isset( $custom_code['body'], $custom_code['body']['value'] ) ) {
815 $code .= $custom_code['body']['value'];
816 }
817 $code .= '</div>';
818 return $code;
819 }
820
821 /**
822 * Returns the css code with `<style>` tag user put into `Page settings`
823 *
824 * @return string
825 */
826 public static function getHeadCustomCode() {
827 $post_id = HelperFunctions::get_post_id_if_possible_from_url();
828 $custom_code = get_post_meta( $post_id, KIRKI_PAGE_CUSTOM_CODE, true );
829 $code = '';
830 if ( isset( $custom_code['head'], $custom_code['head']['value'] ) ) {
831 $code .= "<meta data-kirki-code='start' />";
832 $code .= $custom_code['head']['value'];
833 $code .= "<meta data-kirki-code='end' />";
834 }
835 return $code;
836 }
837
838 public function getElementWiseVariableCssCodes() {
839 $s = '';
840 foreach ( $this->ele_variable_modes as $ele_id => $modes ) {
841 // if ( $mode === 'inherit' ) {
842 // continue;
843 // }
844 $s .= self::getVariableCssCode( $ele_id, '[data-kirki="' . $ele_id . '"]', $modes );
845 }
846 return $s;
847 }
848
849 public static function getVariableCssCode( $key = 'global', $selector = ':root', $modes = [], $style_tag = true ) {
850 $modes = HelperFunctions::normalize_variable_mode( $modes );
851 $s = '';
852 foreach ( $modes as $mode_type => $mode ) {
853 $s .= self::generateSingleVariableModeCssCode($mode_type, $key, $selector, $mode, $style_tag );
854 }
855 return $s;
856 }
857
858 private static function generateSingleVariableModeCssCode($mode_type,$key = 'global', $selector = ':root', $mode=false, $style_tag = true){
859 $k = "$key-$selector-$mode";
860 $variables = UserData::get_kirki_variable_data();
861
862 if ( ! $mode || $mode === 'inherit' ) {
863 $mode = 'default';
864 }
865
866 $s = $style_tag ? "<style id='kirki-variables-" . $key . "'>".$selector."{" : $selector."{";
867 $view_ports = UserData::get_view_port_list();
868
869 $text_style_css = "";
870
871 foreach ($variables['data'] as $key2 => $group) {
872 if($group['key'] !== $mode_type) {
873 continue;
874 }
875 foreach ( $group['variables'] as $key3 => $variable) {
876 if ( ! isset( $variable['value'][ $mode ] ) ) {
877 continue;
878 }
879
880 $name = '--' . $variable['id'];
881
882 switch ( $variable['type'] ) {
883 case 'size':
884 $s .= "$name:" . $variable['value'][$mode]['value'] . $variable['value'][$mode]['unit'] . ";";
885 break;
886 case 'font-family':
887 $font_value = $variable['value'][$mode];
888 if (strpos($font_value, '"') === 0 || strpos($font_value, "'") === 0) {
889 $s .= "$name:" . $font_value . ";";
890 } else {
891 $s .= "$name:\"" . $font_value . "\";";
892 }
893 // $s .= "$name:\"" . $variable['value'][$mode] . "\";";
894 break;
895 case 'color':
896 $s .= "$name:" . $variable['value'][$mode] . ";";
897 break;
898 case 'text-style':
899 $text_style_css .= self::buildTextStyleCss( $variable, $mode, $view_ports );
900 break;
901 }
902 }
903 }
904
905 if($selector && $selector !== ':root'){
906 $text_style_css = "$selector{$text_style_css}";
907 }
908
909 $s .= $style_tag ? "}$text_style_css</style>" : "}$text_style_css";
910
911 self::$printed_variable_tracker[ $k ] = true;
912 return $s;
913 }
914
915 /**
916 * Build CSS rules for a single text-style variable.
917 *
918 * @param array $variable The text-style variable data.
919 * @param string $mode The active mode key.
920 * @param array $view_ports The viewport configuration list.
921 *
922 * @return string
923 */
924 private static function buildTextStyleCss( $variable, $mode, $view_ports ) {
925 $css = '';
926 $mode_data = $variable['value'][ $mode ];
927 $var_id = $variable['id'];
928 $font_family = isset( $mode_data['font-family'] ) ? $mode_data['font-family'] : '';
929 $styles = isset( $mode_data['styles'] ) ? $mode_data['styles'] : array();
930 $ts_selector = '[data-text_style="' . $var_id . '"]';
931
932 foreach ( $styles as $device => $props ) {
933 $css_declarations = '';
934
935 if ( $device === 'md' && $font_family ) {
936 $css_declarations .= 'font-family:' . $font_family . ';';
937 }
938
939 foreach ( $props as $prop_name => $prop_value ) {
940
941 // font-feature-settings
942 if (
943 $prop_name === 'font-feature-settings' &&
944 is_array( $prop_value ) &&
945 isset( $prop_value['value'] ) &&
946 is_array( $prop_value['value'] )
947 ) {
948 $css_string = implode(
949 ', ',
950 array_map(
951 function( $key ) {
952 return '"' . $key . '"';
953 },
954 array_keys( $prop_value['value'] )
955 )
956 );
957
958 $css_declarations .= $prop_name . ':' . $css_string . ';';
959 continue;
960 }
961
962 // value + unit object
963 if (
964 is_array( $prop_value ) &&
965 isset( $prop_value['value'], $prop_value['unit'] )
966 ) {
967
968 // font-size clamp
969 if (
970 $prop_name === 'font-size' &&
971 isset( $prop_value['type'] ) &&
972 str_contains( $prop_value['type'], 'clamp-' )
973 ) {
974
975 $min_value = isset($prop_value['min'], $prop_value['min']['value'] ) ? $prop_value['min']['value'] : 0;
976 $min_unit = isset($prop_value['min'], $prop_value['min']['unit'] ) ? $prop_value['min']['unit'] : 'px';
977
978 $base_value = isset($prop_value['base'], $prop_value['base']['value'] ) ? $prop_value['base']['value'] : 0;
979 $base_unit = isset($prop_value['base'], $prop_value['base']['unit'] ) ? $prop_value['base']['unit'] : 'px';
980
981 $max_value = isset($prop_value['max'], $prop_value['max']['value'] ) ? $prop_value['max']['value'] : 0;
982 $max_unit = isset($prop_value['max'], $prop_value['max']['unit'] ) ? $prop_value['max']['unit'] : 'px';
983
984 $css_declarations .= "{$prop_name}:clamp({$min_value}{$min_unit}, {$base_value}{$base_unit}, {$max_value}{$max_unit});";
985
986 // special max-width values
987 } elseif (
988 $prop_name === 'max-width' &&
989 in_array( $prop_value['value'], array( 'none', 'fit-content', 'max-content', 'min-content' ), true )
990 ) {
991
992 $css_declarations .= $prop_name . ':' . $prop_value['value'] . ';';
993
994 } else {
995
996 $css_declarations .= $prop_name . ':' . $prop_value['value'] . $prop_value['unit'] . ';';
997 }
998
999 // string or number
1000 } elseif (
1001 ( is_string( $prop_value ) && $prop_value !== '' ) ||
1002 is_numeric( $prop_value )
1003 ) {
1004
1005 $css_declarations .= $prop_name . ':' . $prop_value . ';';
1006 }
1007 }
1008
1009 if ( empty( $css_declarations ) ) {
1010 continue;
1011 }
1012
1013 $rule = $ts_selector . '{' . $css_declarations . '}';
1014
1015 if ( $device === 'md' ) {
1016 $css .= $rule;
1017 } elseif ( isset( $view_ports[ $device ] ) ) {
1018 $max_width = $view_ports[ $device ]['maxWidth'];
1019 $css .= '@media only screen and (max-width:' . $max_width . 'px){' . $rule . '}';
1020 }
1021 }
1022
1023 return $css;
1024 }
1025
1026 /**
1027 * Returns `<meta>` tag with provided content
1028 *
1029 * @param string $name The meta name.
1030 * @param string $content The meta description.
1031 * @return string The `<meta />` tag.
1032 */
1033 private static function getMeta( $name, $content ) {
1034 // Replace &nbsp; and \u00a0 with a space
1035 $content = preg_replace( '/(?:&nbsp;|\x{00a0})/u', ' ', $content );
1036
1037 // Escape double quotes in the content to avoid breaking the HTML attribute
1038 $content = htmlspecialchars( $content, ENT_QUOTES, 'UTF-8' );
1039
1040 return "<meta name=\"{$name}\" content=\"{$content}\" />";
1041 }
1042
1043
1044 /**
1045 * All meta tags
1046 *
1047 * @return string
1048 */
1049 public static function getSeoMetaTags( $post_id = null ) {
1050 $post_id = $post_id ? $post_id : HelperFunctions::get_post_id_if_possible_from_url();
1051 $seo_settings = false;
1052
1053 // get set settings from template if current page is kirki template
1054 $template_data = HelperFunctions::get_template_data_if_current_page_is_kirki_template();
1055 if ( $template_data ) {
1056 $seo_settings = get_post_meta( $template_data['template_id'], KIRKI_PAGE_SEO_SETTINGS_META_KEY, true );
1057 } else {
1058 $seo_settings = get_post_meta( $post_id, KIRKI_PAGE_SEO_SETTINGS_META_KEY, true );
1059 }
1060
1061 // $title = esc_html(get_the_title());
1062 $meta_tags = '';
1063
1064 // if ( $seo_settings && $seo_settings['seoSettings'] && $seo_settings['seoSettings']['seoTitleTag'] && $seo_settings['seoSettings']['seoTitleTag']['value'] ) {
1065 // $seo_title = self::getSeoValue($post_id, $seo_settings['seoSettings']['seoTitleTag']['value']);
1066 // $title = $seo_title;
1067 // $meta_tags .= self::getMeta('title', $seo_title);
1068 // }
1069
1070 // $meta_tags .= '<title>' . $title . '</title>';
1071
1072 if ( $seo_settings && $seo_settings['seoSettings'] && $seo_settings['seoSettings']['seoMetaDesc'] && $seo_settings['seoSettings']['seoMetaDesc']['value'] ) {
1073 $seo_meta_desc = self::getSeoValue( $post_id, $seo_settings['seoSettings']['seoMetaDesc']['value'] );
1074 $meta_tags .= self::getMeta( 'description', $seo_meta_desc );
1075 }
1076
1077 if ( $seo_settings && $seo_settings['openGraph'] && $seo_settings['openGraph']['openGraphImage'] && $seo_settings['openGraph']['openGraphImage']['value'] ) {
1078 $seo_open_graph_image = self::getSeoValue( $post_id, $seo_settings['openGraph']['openGraphImage']['value'] );
1079
1080 $meta_tags .= self::getMeta( 'og:image', $seo_open_graph_image );
1081 $meta_tags .= self::getMeta( 'twitter:card', 'summary_large_image' );
1082 $meta_tags .= self::getMeta( 'twitter:image', $seo_open_graph_image );
1083 }
1084
1085 if ( $seo_settings && $seo_settings['openGraph'] && $seo_settings['openGraph']['openGraphTitle'] ) {
1086 // If og:title is sameAsSeoTitle.
1087 if (
1088 $seo_settings['openGraph']['openGraphTitle']['sameAsSeoTitle'] &&
1089 $seo_settings && $seo_settings['seoSettings'] && $seo_settings['seoSettings']['seoTitleTag'] && $seo_settings['seoSettings']['seoTitleTag']['value']
1090 ) {
1091 $seo_title = self::getSeoValue( $post_id, $seo_settings['seoSettings']['seoTitleTag']['value'] );
1092
1093 $meta_tags .= self::getMeta( 'og:title', $seo_title );
1094 $meta_tags .= self::getMeta( 'twitter:title', $seo_title );
1095 } elseif ( $seo_settings['openGraph']['openGraphTitle']['value'] ) {
1096
1097 $og_title = self::getSeoValue( $post_id, $seo_settings['openGraph']['openGraphTitle']['value'] );
1098
1099 $meta_tags .= self::getMeta( 'og:title', $og_title );
1100 $meta_tags .= self::getMeta( 'twitter:title', $og_title );
1101 }
1102 }
1103
1104 if ( $seo_settings && $seo_settings['openGraph'] && $seo_settings['openGraph']['openGraphDesc'] ) {
1105 // If og:description is sameAsSeoMeta.
1106 if (
1107 $seo_settings['openGraph']['openGraphDesc']['sameAsSeoMeta'] &&
1108 $seo_settings && $seo_settings['seoSettings'] && $seo_settings['seoSettings']['seoMetaDesc'] && $seo_settings['seoSettings']['seoMetaDesc']['value']
1109 ) {
1110 $seo_meta_desc = self::getSeoValue( $post_id, $seo_settings['seoSettings']['seoMetaDesc']['value'] );
1111
1112 $meta_tags .= self::getMeta( 'og:description', $seo_meta_desc );
1113 $meta_tags .= self::getMeta( 'twitter:description', $seo_meta_desc );
1114 } elseif ( $seo_settings['openGraph']['openGraphDesc']['value'] ) {
1115
1116 $og_meta_des = self::getSeoValue( $post_id, $seo_settings['openGraph']['openGraphDesc']['value'] );
1117
1118 $meta_tags .= self::getMeta( 'og:description', $og_meta_des );
1119 $meta_tags .= self::getMeta( 'twitter:description', $og_meta_des );
1120 }
1121 }
1122
1123 $meta_tags .= self::getMeta( 'og:url', get_permalink( $post_id ) );
1124 return $meta_tags;
1125 }
1126
1127 /**
1128 * Get the seo title
1129 *
1130 * @param int $post_id post id.
1131 * @param string|array $value seo title tag value.
1132 *
1133 * @return string
1134 */
1135
1136 public static function getSeoValue( $post_id, $value ) {
1137 $seo_value = '';
1138
1139 if ( is_array( $value ) ) {
1140 $post = get_post( $post_id );
1141 $post_parent_id = null;
1142 // if template preview open
1143 if ( $post->post_type && str_contains( $post->post_type,'kirki_template' ) ) {
1144 // get post condition
1145 $post_conditions = get_post_meta( $post->ID,'kirki_template_conditions', true ); // kirki_cm_parentId
1146 $condition = $post_conditions[0];
1147 if (
1148 isset( $condition['post_type'] ) &&
1149 ! empty( $condition['post_type'] )
1150 ) {
1151 if ( strpos( $condition['post_type'], KIRKI_CONTENT_MANAGER_PREFIX ) !== false ) {
1152 // content manager related post
1153 $post_parent_id = str_replace( KIRKI_CONTENT_MANAGER_PREFIX . '_', '', $condition['post_type'] );
1154
1155 $args = array(
1156 'post_parent' => $post_parent_id,
1157 'page' => 1,
1158 'posts_per_page' => 1,
1159 );
1160
1161 $res = ContentManagerHelper::get_all_child_items( $args );
1162
1163 if ( $res && $res[0] ) {
1164 $post = (object) $res[0];
1165 }
1166 }
1167 }
1168 }
1169
1170 foreach ( $value as $key => $val ) {
1171 $option_type = $val['type'];
1172 $option_value = $val['value'];
1173
1174 if ( $option_type === 'text' ) {
1175 $seo_value .= $option_value;
1176 } elseif ( $option_type === 'post' ) {
1177 // post author
1178 if ( isset( $post->$option_value ) && $option_value === 'post_author' ) {
1179 $seo_value .= get_the_author_meta( 'display_name', $post->post_author );
1180 } elseif ( $option_value === 'post_id' ) {
1181 $seo_value .= isset( $post->ID ) ? $post->ID : '';
1182 } else {
1183 $seo_value .= isset( $post->$option_value ) ? $post->$option_value : '';
1184 }
1185 } elseif ( $option_type === KIRKI_CONTENT_MANAGER_PREFIX . '_field' ) {
1186 $meta_key = ContentManagerHelper::get_child_post_meta_key_using_field_id( $post->post_parent, $option_value );
1187
1188 $post_meta_value = get_post_meta( $post->ID, $meta_key, true );
1189
1190 // for image field
1191 if ( $post_meta_value && isset( $post_meta_value['url'] ) ) {
1192 $post_meta_value = $post_meta_value['url'];
1193 }
1194
1195 $seo_value .= wp_strip_all_tags( $post_meta_value );
1196 } elseif ( $option_type === 'featured_image' ) {
1197 $seo_value .= get_the_post_thumbnail_url( $post->ID );
1198 } elseif ( $option_type === 'user' ) {
1199 $user_id = HelperFunctions::get_user_id_if_possible_from_url();
1200
1201 if ( $option_value === 'user_id' ) {
1202 $seo_value .= $user_id;
1203 } elseif ( $option_value === 'user_name' ) {
1204 $seo_value .= get_the_author_meta( 'display_name', $user_id );
1205 } else {
1206 $seo_value .= get_the_author_meta( $option_value, $user_id );
1207 }
1208 } elseif ( $option_type === 'term' ) {
1209 $term_id = HelperFunctions::get_term_id_if_possible_from_url();
1210 if ( $term_id ) {
1211 $term = get_term( $term_id );
1212
1213 if ( $option_value === 'term_id' ) {
1214 $seo_value .= $term->term_id;
1215 } elseif ( $option_value === 'term_name' ) {
1216 $seo_value .= $term->name;
1217 } elseif ( $option_value === 'term_slug' ) {
1218 $seo_value .= $term->slug;
1219 }
1220 }
1221 }
1222 }
1223 } else {
1224 $seo_value = $value;
1225 }
1226
1227 $seo_value = do_shortcode( $seo_value );
1228
1229 return $seo_value;
1230 }
1231
1232 /**
1233 * Check if has pseudo class value
1234 *
1235 * @param string $s pseudo class name like => active, hover, focus, child(5th).
1236 *
1237 * @return string
1238 */
1239 private function checkIfHasPsuedoClassValue( $s ) {
1240 if ( strpos( $s, '-----' ) !== false ) {
1241 $s_arr = explode( '-----', $s );
1242 $s = $s_arr[0] . '(' . $s_arr[1] . ')';
1243 }
1244 if(in_array($s, KIRKI_PRESERVED_CLASS_LIST)){
1245 return ".$s";
1246 }
1247 $pseudoClass = str_contains( $s, 'before' ) || str_contains( $s, 'after' ) || str_contains( $s, 'placeholder' ) ? '::' . $s : ':' . $s;
1248 return $pseudoClass;
1249 }
1250
1251
1252 /**
1253 * Get the selector from block
1254 *
1255 * @param array $block single style block.
1256 *
1257 * @return string
1258 */
1259 private function getSelectorFromBlock( $block ) {
1260 $selector = '';
1261 if ( isset( $block['type'] ) && $block['type'] === 'class' ) {
1262 $block['name'] = HelperFunctions::add_prefix_to_class_name( $this->prefix, $block['name'] );
1263 if ( is_array( $block['name'] ) ) {
1264 $selector = '.' . str_replace( ' ', '.', $this->makeClassStringFromArray( $block['name'] ) );
1265 } else {
1266 $selector = '.' . $this->makeClassStringFromArray( array( $block['name'] ) );
1267 }
1268 } elseif ( isset( $block['type'] ) && $block['type'] === 'tag' ) {
1269 $selector = isset( $block['tag'] ) ? $block['tag'] : ( isset( $block['name'] ) ? $block['name'] : '' );
1270 }
1271 return $selector;
1272 }
1273
1274 /**
1275 * Insert element related data if needed
1276 *
1277 * @param array $element single element data.
1278 * @return void
1279 */
1280 private function insertElementRelatedConfig( $element, $options = array() ) {
1281 if ( ! $element || !isset( $element['id'] ) ) {
1282 return;
1283 }
1284 $id = $element['id'];
1285 if ( isset( $element['properties'] ) ) {
1286 $properties = $element['properties'];
1287
1288 if ( isset( $properties['interactions'] ) ) {
1289 $this->interactions[ $id ] = $this->updateClassListForInteractionFromStyleBlockId( $properties['interactions'], $element );
1290 }
1291 if(isset($properties['interactionLibrary'])) {
1292 $this->interaction_library[ $id ] = $this->updateStylesForInteractionLibrary( $properties['interactionLibrary'], $element );
1293 }
1294 if ( isset( $properties['code'], $properties['code']['javascript'] ) ) {
1295 $this->custom_codes .= str_replace( 'KIRKI_TARGET_ELEMENT_ID', $id, $properties['code']['javascript'] );
1296 }
1297
1298 if ( isset( $properties['variableMode'] ) ) {
1299 $this->ele_variable_modes[ $id ] = $properties['variableMode'];
1300 }
1301
1302 // Store slider properties.
1303 if ( $element['name'] === 'slider' ) {
1304 $this->sliders[ $id ] = $properties['slider'];
1305 }
1306
1307 if ( $element['name'] === 'popup' ) {
1308 $this->popups[ $id ] = $properties['popup'];
1309 }
1310
1311 if ( $element['name'] === 'map' || $element['name'] === 'google-map' ) {
1312 $this->maps[ $id ] = $properties['map'];
1313 }
1314
1315 if ( $element['name'] === 'video' ) {
1316 $this->videos[ $id ] = $properties['attributes'];
1317 }
1318 // Store Lottie properties.
1319 if ( $element['name'] === 'lottie' ) {
1320 $this->lotties[ $id ] = $properties['lottie'];
1321 }
1322 // Store Lottie properties.
1323 if ( $element['name'] === 'dropdown' ) {
1324 $this->dropdown[ $id ] = $properties['dropdown'];
1325 }
1326
1327 if ( $element['name'] === 'tabs' ) {
1328 $this->tabs[ $id ]['active_tab'] = $properties['active_tab'];
1329 $this->tabs[ $id ]['animationName'] = $properties['animationName'];
1330 $this->tabs[ $id ]['easing'] = isset( $properties['easing'] ) ? $properties['easing'] : 'ease';
1331 $this->tabs[ $id ]['duration'] = isset( $properties['duration'] ) ? $properties['duration'] : 100;
1332 }
1333
1334 if ( $element['name'] === 'lightbox' ) {
1335 $this->lightboxes[ $id ] = $properties['lightbox'];
1336 }
1337
1338 if ( $element['name'] === 'collection' ) {
1339 $this->collections[$id] = array_merge(
1340 $properties['dynamicContent'],
1341 array(
1342 'isSearchableCollection' => isset($options['search_related_collection_ids'][$id]) ? true : false,
1343 )
1344 );
1345 }
1346
1347 // navigation properties.
1348 if ( $element['name'] === 'navigation' ) {
1349 $this->navigations[ $id ] = $properties['navigation'];
1350 }
1351
1352 // navigation item properties.
1353 if ( $element['name'] === 'navigation-item' ) {
1354 $this->navigation_item[ $id ] = isset( $properties['navigationItem'] ) ? $properties['navigationItem'] : array(); // TODO:
1355 }
1356
1357 // input properties.
1358 if ( $element['name'] === 'input' ) {
1359 $this->inputs[ $id ] = $properties;
1360 }
1361
1362 if (
1363 $element['name'] === 'input' ||
1364 $element['name'] === 'textarea' ||
1365 $element['name'] === 'select' ||
1366 $element['name'] === 'checkbox-element' ||
1367 $element['name'] === 'radio-group'
1368 ) {
1369 $parent_form_id = $options['form']['id'] ?? '';
1370 $session_data = HelperFunctions::get_session_data( $parent_form_id );
1371
1372 $type = $element['properties']['attributes']['type'] ?? '';
1373 $others_attributes = array();
1374
1375 if ( $session_data && isset( $element['properties']['attributes']['name'] ) ) {
1376 if ( ! isset( $session_data['fields'] ) ) {
1377 $session_data['fields'] = array(
1378 $element['properties']['attributes']['name'] => array_merge(
1379 array(
1380 'type' => $type,
1381 'required' => $element['properties']['attributes']['required'] ?? false,
1382 ),
1383 $element['properties']['attributes'],
1384 $others_attributes,
1385 ),
1386 );
1387 } else {
1388 $session_data['fields'][ $element['properties']['attributes']['name'] ] = array_merge(
1389 array(
1390 'type' => $type,
1391 'required' => $element['properties']['attributes']['required'] ?? false,
1392 ),
1393 $element['properties']['attributes'],
1394 $others_attributes,
1395 );
1396 }
1397
1398 HelperFunctions::set_session_data( $parent_form_id, $session_data );
1399 }
1400 }
1401 if ( $element['name'] === 'form' ) {
1402 $this->forms[ $id ] = array_merge( $properties['form'], $properties['attributes'] );
1403 $this->check_popup_inside_form( $properties['form'] );
1404 HelperFunctions::set_session_data( $id, array_merge( array( 'id' => $id ), $this->forms[ $id ] ) );
1405 }
1406
1407 if ( $element['name'] === 'button' ) {
1408 $this->check_popup_inside_button( $properties );
1409 }
1410
1411 if ( $element['name'] === 'button' || $element['name'] === 'link-text' || $element['name'] === 'link-block' ) {
1412 $this->check_popup_inside_button( $properties );
1413 }
1414
1415 if ( $element['name'] === 'recaptcha' ) {
1416 if (!isset($properties['recaptcha'], $properties['recaptcha']['GRC_version'])) {
1417 return;
1418 }
1419
1420 $parent_form_id = $options['form']['id'] ?? '';
1421 $session_data = HelperFunctions::get_session_data($parent_form_id);
1422
1423 $version = $properties['recaptcha']['GRC_version'];
1424 $grc_site_key = $properties['recaptcha']['GRC_site_key'];
1425
1426 $this->re_captchas[$id]['data-version'] = $version;
1427 $this->re_captchas[$id]['data-sitekey'] = $grc_site_key;
1428
1429 // set recaptcha version and site key at form session data
1430 if ($session_data) {
1431 $session_data['recaptcha'] = array(
1432 'GRC_version' => $properties['recaptcha']['GRC_version'],
1433 'GRC_site_key' => $properties['recaptcha']['GRC_site_key'],
1434 'GRC_secret_key' => $properties['recaptcha']['GRC_secret_key'],
1435 );
1436
1437 HelperFunctions::set_session_data($parent_form_id, $session_data);
1438 }
1439 }
1440 }
1441 }
1442
1443 private function getClassNameAndAnimationStyle( $animations, $styleBlockId ) {
1444 if ( ! empty( $this->style_blocks[ $styleBlockId ] ) ) {
1445 $className = $this->style_blocks[ $styleBlockId ]['name'];
1446
1447 if ( $className ) {
1448 // If className is an array, join all values with a dot separator
1449 $classSelector = HelperFunctions::get_selector_from_sb_name( $className );
1450 $innerCSS = '';
1451 foreach ( $animations as $animation ) {
1452 if ( ! empty( $animation['setAsInitial'] ) && $animation['setAsInitial'] ) {
1453 $temp = $this->processElementStyles( $classSelector, $animation );
1454 $innerCSS .= $this->setAnimationStyles( $temp, $hasClassName = true );
1455 }
1456 }
1457 if ( $innerCSS ) {
1458 return "$classSelector { $innerCSS }";
1459 }
1460 }
1461 }
1462
1463 return ''; // Return an empty string if no className is found
1464 }
1465
1466
1467 private function setMediaQuery( $cssValue, $deviceList ) {
1468 $media_queries = '';
1469 $viewPorts = $this->view_ports;
1470
1471 // Sort viewports by value ascending
1472 uasort(
1473 $viewPorts,
1474 function( $a, $b ) {
1475 return $a['value'] <=> $b['value'];
1476 }
1477 );
1478
1479 // Get sorted keys
1480 $orderedKeys = array_keys( $viewPorts );
1481 $ranges = array();
1482 $prev_viewport = null;
1483 $count = 0;
1484
1485 foreach ( $viewPorts as $key => $vp ) {
1486 if ( in_array( $key, $deviceList ) ) {
1487 if ( $count === 0 && $vp['value'] < 1200 ) {
1488 $ranges[] = array(
1489 'min' => null,
1490 'max' => $vp['value'],
1491 );
1492 } elseif ( $vp['value'] < 1200 ) {
1493 $ranges[] = array(
1494 'min' => $prev_viewport['value'] + 1,
1495 'max' => $vp['value'],
1496 );
1497 } elseif ( $count === count( $viewPorts ) - 1 && $vp['value'] > 1200 ) {
1498 $ranges[] = array(
1499 'min' => $vp['value'],
1500 'max' => null,
1501 );
1502 } elseif ( $vp['value'] > 1200 ) {
1503 $ranges[] = array(
1504 'min' => $prev_viewport['value'] + 1,
1505 'max' => $vp['value'],
1506 );
1507 } else {
1508 // this is md device
1509 if ( $count === count( $viewPorts ) - 1 ) {
1510 // thats means: no md upper device like: 1600
1511 $ranges[] = array(
1512 'min' => $prev_viewport['value'] + 1,
1513 'max' => null,
1514 );
1515 } else {
1516 // next viewport
1517 $next_viewport = $viewPorts[ $orderedKeys[ $count + 1 ] ];
1518 $ranges[] = array(
1519 'min' => $prev_viewport['value'] + 1,
1520 'max' => $next_viewport['value'] - 1,
1521 );
1522 }
1523 }
1524 }
1525 $count++;
1526 $prev_viewport = $vp;
1527 }
1528 foreach ( $ranges as $key => $value ) {
1529 $min = $value['min'];
1530 $max = $value['max'];
1531 if ( $min !== null && $max !== null ) {
1532 $media_queries .= "@media (min-width: {$min}px) and (max-width: {$max}px) { $cssValue } ";
1533 } elseif ( $min !== null ) {
1534 $media_queries .= "@media (min-width: {$min}px) { $cssValue } ";
1535 } elseif ( $max !== null ) {
1536 $media_queries .= "@media (max-width: {$max}px) { $cssValue } ";
1537 }
1538 }
1539
1540 return $media_queries;
1541 }
1542
1543
1544
1545 private function getMediaQuery( $cssValue, $single_res_value ) {
1546 return isset( $single_res_value['deviceAndClassList']['devices'] ) && count( $single_res_value['deviceAndClassList']['devices'] ) > 0
1547 ? $this->setMediaQuery( $cssValue, $single_res_value['deviceAndClassList']['devices'] )
1548 : '';
1549 }
1550
1551 private function setInteractionCssValueToTracker( $classApplyTo, $element, $ele_id, $animations, $styleBlockId, $single_res_value ) {
1552 $parent_selector = false;
1553 if ( isset( $single_res_value['deviceAndClassList'], $single_res_value['deviceAndClassList']['applyToClass'], $single_res_value['deviceAndClassList']['styleBlockId'] ) && $single_res_value['deviceAndClassList']['applyToClass'] ) {
1554 if ( isset( $this->style_blocks[ $single_res_value['deviceAndClassList']['styleBlockId'] ] ) ) {
1555 $sb = $this->style_blocks[ $single_res_value['deviceAndClassList']['styleBlockId'] ];
1556 $parent_selector = HelperFunctions::get_selector_from_sb_name( $sb['name'] );
1557 }
1558 }
1559
1560 $cssValue = $this->getClassNameAndAnimationStyle( $animations, $styleBlockId );
1561
1562 if ( ! $cssValue ) {
1563 return;
1564 }
1565
1566 switch ( $classApplyTo ) {
1567
1568 case 'childrens':
1569 $parentID = $element['id'];
1570
1571 if ( ! $parent_selector ) {
1572 $parent_selector = "[data-kirki='" . $parentID . "']";
1573 }
1574 $animation_css = $parent_selector . ' ' . $cssValue;
1575 $media_queries = $this->getMediaQuery( $animation_css, $single_res_value );
1576
1577 $finalCss = ! empty( $media_queries ) ? $media_queries : $animation_css;
1578 $this->track_animation_for_children_with_this_class .= $finalCss;
1579
1580 break;
1581
1582 case 'siblings':
1583 if ( $cssValue ) {
1584 $child_sb = $this->style_blocks[ $styleBlockId ];
1585 $child_selector = HelperFunctions::get_selector_from_sb_name( $child_sb['name'] );
1586 if ( $parent_selector ) {
1587 $animation_css = "$parent_selector $child_selector ~ $cssValue";
1588 } else {
1589 // $animation_css = "$child_selector ~ $cssValue";
1590 $animation_css = "[data-kirki='" . $ele_id . "'] ~" . ' ' . $cssValue;
1591 }
1592
1593 $media_queries = $this->getMediaQuery( $animation_css, $single_res_value );
1594 $finalCss = ! empty( $media_queries ) ? $media_queries : $animation_css;
1595 $this->track_animation_for_sibling_with_this_class .= $finalCss;
1596 }
1597 break;
1598
1599 case 'trigger-siblings':
1600 $parentID = $element['parentId'];
1601
1602 if ( ! $parent_selector ) {
1603 $parent_selector = "[data-kirki='" . $parentID . "']";
1604 }
1605 $animation_css = "$parent_selector > " . $cssValue;
1606 $media_queries = $this->getMediaQuery( $animation_css, $single_res_value );
1607 $finalCss = ! empty( $media_queries ) ? $media_queries : $animation_css;
1608 $this->track_animation_for_trigger_sibling_with_this_class .= $finalCss;
1609
1610 break;
1611
1612 case 'trigger':
1613 $media_queries = $this->getMediaQuery( $cssValue, $single_res_value );
1614 $finalCss = ! empty( $media_queries ) ? $media_queries : $cssValue;
1615 $this->track_animation_for_trigger_with_this_class .= $finalCss;
1616
1617 break;
1618
1619 default:
1620 $media_queries = $this->getMediaQuery( $cssValue, $single_res_value );
1621 $finalCss = ! empty( $media_queries ) ? $media_queries : $cssValue;
1622 $this->track_animation_for_elements_with_this_class .= $finalCss;
1623 break;
1624
1625 }
1626 }
1627
1628
1629 private function updateClassListForInteractionFromStyleBlockId( $interactionData, $element ) {
1630 $id = $element['id'];
1631 foreach ( $interactionData as $el_as_target_key => $el_as_target_value ) {
1632 if ( $el_as_target_key === 'deviceAndClassList' ) {
1633 if ( isset( $interactionData['deviceAndClassList']['styleBlockId'] ) ) {
1634 $this_block = isset( $this->style_blocks[ $interactionData['deviceAndClassList']['styleBlockId'] ] ) ? $this->style_blocks[ $interactionData['deviceAndClassList']['styleBlockId'] ] : false;
1635 if ( $this_block ) {
1636 $class_names = array();
1637 if ( is_array( $this_block['name'] ) ) {
1638 $class_names = HelperFunctions::add_prefix_to_class_name( $this->prefix, $this_block['name'] );
1639 } else {
1640 $class_names[] = HelperFunctions::add_prefix_to_class_name( $this->prefix, $this_block['name'] );
1641 }
1642 $interactionData['deviceAndClassList']['classList'] = $class_names;
1643 $this->add_to_only_used_style_blocks( $this_block );
1644 }
1645 }
1646 } elseif ( $el_as_target_key === 'elementAsTrigger' ) {
1647 foreach ( $el_as_target_value as $event_key => $event_value ) {
1648 foreach ( $event_value as $custom_or_preset_key => $custom_or_preset_value ) {
1649 foreach ( $custom_or_preset_value as $single_res_key => $single_res_value ) {
1650 $new_data = array();
1651 foreach ( $single_res_value['data'] as $ele_id => $animations ) {
1652 if ( strpos( $ele_id, '____info' ) !== false ) {
1653 continue;
1654 }
1655
1656 if ( ( $custom_or_preset_key === 'preset' || $custom_or_preset_key === 'textAnimation' ) && ! empty( $animations ) && count( $animations ) > 0 ) {
1657 if ( $event_key === 'scroll-into-ele' || $event_key === 'scroll-out-ele' ) {
1658 $this->interaction_preset_and_text_animation_tracker[ $ele_id ] = true;
1659 }
1660 }
1661 // set initial value for custom interaction
1662 $this->setInitialAnimation( $ele_id, $animations, $single_res_value );
1663
1664 foreach ( $animations as $ani_key => $animation ) {
1665
1666 if ( isset( $animation['property'] ) && $animation['property'] === 'class-change' ) {
1667 if ( isset( $animation['end'], $animation['end']['className'], $animation['end']['className']['id'] ) ) {
1668 $this_block = isset( $this->style_blocks[ $animation['end']['className']['id'] ] ) ? $this->style_blocks[ $animation['end']['className']['id'] ] : false;
1669 if ( $this_block ) {
1670
1671 $class_names = array();
1672 if ( is_array( $this_block['name'] ) ) {
1673 $class_names = HelperFunctions::add_prefix_to_class_name( $this->prefix, $this_block['name'] );
1674 } else {
1675 $class_names[] = HelperFunctions::add_prefix_to_class_name( $this->prefix, $this_block['name'] );
1676 }
1677
1678 $animation['end']['className']['name'] = $class_names;
1679 $this->add_to_only_used_style_blocks( $this_block );
1680 }
1681 }
1682 }
1683 $animations[ $ani_key ] = $animation;
1684 }
1685 $new_data[ $ele_id ] = $animations;
1686 if ( isset( $single_res_value['data'][ $ele_id . '____info' ] ) ) {
1687
1688 if ( isset( $single_res_value['data'][ $ele_id . '____info' ]['styleBlockId'] ) ) {
1689 $classApplyTo = isset( $single_res_value['data'][ $ele_id . '____info' ]['classApplyOnly'] ) ? $single_res_value['data'][ $ele_id . '____info' ]['classApplyOnly'] : '*';
1690 $applyToClass = isset( $single_res_value['data'][ $ele_id . '____info' ]['applyToClass'] ) ? $single_res_value['data'][ $ele_id . '____info' ]['applyToClass'] : false;
1691 $styleBlockId = $single_res_value['data'][ $ele_id . '____info' ]['styleBlockId'];
1692
1693 if ( $applyToClass ) {
1694 $this->setInteractionCssValueToTracker( $classApplyTo, $element, $ele_id, $animations, $styleBlockId, $single_res_value );
1695 }
1696
1697 $this_block = isset( $this->style_blocks[ $single_res_value['data'][ $ele_id . '____info' ]['styleBlockId'] ] ) ? $this->style_blocks[ $single_res_value['data'][ $ele_id . '____info' ]['styleBlockId'] ] : false;
1698 if ( $this_block ) {
1699
1700 $class_names = array();
1701 if ( is_array( $this_block['name'] ) ) {
1702 $class_names = HelperFunctions::add_prefix_to_class_name( $this->prefix, $this_block['name'] );
1703 } else {
1704 $class_names[] = HelperFunctions::add_prefix_to_class_name( $this->prefix, $this_block['name'] );
1705 }
1706
1707 $single_res_value['data'][ $ele_id . '____info' ]['classList'] = $class_names;
1708
1709 $new_data[ $ele_id . '____info' ] = $single_res_value['data'][ $ele_id . '____info' ];
1710
1711 $this->add_to_only_used_style_blocks( $this_block );
1712 }
1713 } else {
1714 // legecy code support
1715 $new_data[ $ele_id . '____info' ] = $single_res_value['data'][ $ele_id . '____info' ];
1716 }
1717 }
1718 }
1719 $interactionData[ $el_as_target_key ][ $event_key ][ $custom_or_preset_key ][ $single_res_key ]['data'] = $new_data;
1720 }
1721 }
1722 }
1723 }
1724 }
1725 return $interactionData;
1726 }
1727
1728 private function updateStylesForInteractionLibrary( $interactionLibraryData, $element ) {
1729 $id = $element['id'];
1730 foreach ( $interactionLibraryData as $interactionLibrary ) {
1731 if ( isset( $interactionLibrary['type'] ) && $interactionLibrary['type'] === 'textAnimation' ) {
1732 if( isset($interactionLibrary['trigger']) && $interactionLibrary['trigger'] === 'scrollIntoView' ) {
1733 $devices = isset( $interactionLibrary['deviceAndClassList']['devices'] ) ? $interactionLibrary['deviceAndClassList']['devices'] : array();
1734 $this->interaction_library_text_animation_tracker[ $id ] = $devices;
1735 break;
1736 }
1737 }
1738 }
1739 return $interactionLibraryData;
1740 }
1741
1742 /**
1743 * Get element related config
1744 *
1745 * @return array
1746 */
1747 public function get_element_related_config() {
1748 return array(
1749 'interactions' => $this->interactions,
1750 'sliders' => $this->sliders,
1751 'popups' => $this->popups,
1752 'maps' => $this->maps,
1753 'lotties' => $this->lotties,
1754 'tabs' => $this->tabs,
1755 'lightboxes' => $this->lightboxes,
1756 'collections' => $this->collections,
1757 'forms' => $this->forms,
1758 'reCaptchas' => $this->re_captchas,
1759 'dropdown' => $this->dropdown,
1760 );
1761 }
1762
1763 /**
1764 * Merge element related config.
1765 *
1766 * Set/merge all element releted cofig.
1767 *
1768 * @param array $configs element congiguration.
1769 *
1770 * @return void
1771 */
1772 public function merge_element_related_config( $configs ) {
1773 $this->interactions = array_merge( $this->interactions, $configs['interactions'] );
1774 $this->sliders = array_merge( $this->sliders, $configs['sliders'] );
1775 $this->popups = array_merge( $this->popups, $configs['popups'] );
1776 $this->maps = array_merge( $this->maps, $configs['maps'] );
1777 $this->lotties = array_merge( $this->lotties, $configs['lotties'] );
1778 $this->tabs = array_merge( $this->tabs, $configs['tabs'] );
1779 $this->lightboxes = array_merge( $this->lightboxes, $configs['lightboxes'] );
1780 $this->collections = array_merge( $this->collections, $configs['collections'] );
1781 $this->forms = array_merge( $this->forms, $configs['forms'] );
1782 $this->re_captchas = array_merge( $this->re_captchas, $configs['reCaptchas'] );
1783 $this->dropdown = array_merge( $this->dropdown, $configs['dropdown'] );
1784 }
1785
1786 /**
1787 * Check popup inside button element. Insert the popup inside data.
1788 *
1789 * @param array $button_properties button properties.
1790 *
1791 * @return void
1792 */
1793 private function check_popup_inside_button( $button_properties ) {
1794 // Search for popups.
1795 $type = isset( $button_properties['type'] ) ? $button_properties['type'] : '';
1796
1797 if ( $type === 'popup' ) {
1798 $popup_id = (int) $button_properties['attributes']['popup'];
1799 if ( is_numeric( $popup_id ) ) {
1800 $this->insert_popup_into_data( $popup_id );
1801 }
1802 }
1803 }
1804
1805 /**
1806 * Check popup inside form element. then insert the popup data.
1807 *
1808 * @param array $form_properties form properties.
1809 * @return void
1810 */
1811 private function check_popup_inside_form( $form_properties ) {
1812 // Search for popups.
1813 $action_on_submit = isset( $form_properties['onSubmit']['type'] ) ? $form_properties['onSubmit']['type'] : '';
1814
1815 $action_on_submit_success = isset( $form_properties['onSubmit']['value']['success'] ) ? $form_properties['onSubmit']['value']['success'] : '';
1816
1817 $action_on_submit_fail = isset( $form_properties['onSubmit']['value']['fail'] ) ? $form_properties['onSubmit']['value']['fail'] : '';
1818
1819 if ( $action_on_submit === 'popup' ) {
1820 if ( is_numeric( $action_on_submit_success ) ) {
1821 $this->insert_popup_into_data( $action_on_submit_success );
1822 }
1823
1824 if ( is_numeric( $action_on_submit_fail ) ) {
1825 $this->insert_popup_into_data( $action_on_submit_fail );
1826 }
1827 }
1828 }
1829
1830 /**
1831 * Insert popup into data
1832 *
1833 * @param int $popup_id popup post id.
1834 *
1835 * @return void
1836 */
1837 private function insert_popup_into_data( $popup_id ) {
1838 if ( in_array( $popup_id, self::$only_used_popup_id_array, true ) ) {
1839 return;
1840 }
1841 self::$only_used_popup_id_array[] = $popup_id;
1842 }
1843
1844 /**
1845 * Recursive function to generate html string
1846 *
1847 * @param int $id root id.
1848 * @param array $options elements option.
1849 *
1850 * @return string
1851 */
1852 public function recGenHTML( $id, $options = array() ) {
1853 if ( ! isset( $this->data[ $id ] ) ) {
1854 return '';
1855 }
1856 $this_data = $this->data[ $id ];
1857 if ( isset( $this_data['hide'] ) && $this_data['hide'] === true ) {
1858 return '';
1859 }
1860
1861 if ( isset( $this_data['properties'], $this_data['properties']['access'] ) ) {
1862 if ( ! ( isset( $options['check_access'] ) && $options['check_access'] === false ) && ! HelperFunctions::is_element_accessible( $this_data['properties']['access'] ) ) {
1863 return '';
1864 }
1865 }
1866
1867 if ( isset( $this_data['properties'], $this_data['properties']['visibilityConditions'] ) ) {
1868 if ( ! HelperFunctions::checkVisibilityConditions( $this_data, $options ) ) {
1869 return '';
1870 }
1871 }
1872
1873 $options = array_merge(
1874 $options,
1875 ['element_id' => $id]
1876 );
1877
1878 $this->insertElementRelatedConfig( $this_data, $options );
1879
1880 if ( isset( $this_data['source'] ) && $this_data['source'] !== 'kirki' ) {
1881 // if souce is not equal kirki then the element came from external plugin
1882 return $this->external_element_apply_filter_hook( $this_data['source'], $this_data, $options );
1883 } else {
1884 if(isset($this_data['name'])){
1885 $html = $this->external_element_apply_filter_hook( $this_data['name'], $this_data, $options );
1886 if ( $html ) {
1887 return $html;
1888 }
1889 }
1890 }
1891 return $this->generateSingleElement( $this_data, $options );
1892 }
1893
1894 public function generateSingleElement( $this_data, $options ) {
1895 if (isset( $this_data['name'] ) && in_array( $this_data['name'], $this->exceptional_elements, true ) ) {
1896 if ( ! in_array( $this_data['name'], $this->exceptional_elements_contains_dyn_content, true ) || isset( $this_data['properties']['dynamicContent'] ) ) {
1897 return $this->get_this_exceptional_element( $this_data, $this->getAllAttributes( $this_data ), $options );
1898 }
1899 }
1900 $html = '';
1901 if ( isset( $this_data['properties'], $this_data['properties']['noEndTag'] ) ) {
1902 $html = $this->noEndTagElements( $this_data, $options );
1903 } else {
1904 $html = $this->hasEndTagElements( $this_data, $options );
1905 }
1906 return $html;
1907 }
1908
1909 public function external_element_apply_filter_hook( $source, $this_data, $options ) {
1910 return apply_filters(
1911 'kirki_element_generator_' . $source,
1912 false,
1913 array(
1914 'element' => $this_data,
1915 'elements' => $this->data,
1916 'style_blocks' => $this->style_blocks,
1917 'attributes' => $this->getAllAttributes( $this_data ),
1918 'options' => $options,
1919 'generate_child_element' => array( $this, 'recGenHTML' ),
1920 'generate_element' => array( $this, 'generateSingleElement' ),
1921 'generate_child_element_with_new_id' => array( new HelperFunctions(), 'rec_update_data_id_then_return_new_html' ),
1922 'get_data_and_styles_from_root' => array( new DataHelper(), 'get_data_and_styles_from_root' ),
1923 'get_collection_info' => array( $this, 'get_collection_info' ),
1924 )
1925 );
1926 }
1927
1928
1929 /**
1930 * Elements for has end tag
1931 *
1932 * @param array $this_data single element data.
1933 * @param array $options element options data.
1934 *
1935 * @return string html markup.
1936 */
1937 private function hasEndTagElements( $this_data, $options ) {
1938 $tag = isset( $this_data['properties'], $this_data['properties']['tag'] ) ? $this_data['properties']['tag'] : 'div';
1939 $attributes = $this->getAllAttributes( $this_data );
1940 $preload_link = '';
1941
1942 if ( isset($this_data['name']) && $this_data['name'] == 'link-text' ) {
1943 $href = $this->get_href_value( $this_data, $options );
1944
1945 if ( $href ) {
1946 // replace href.
1947 $attributes = preg_replace( '/href="([^"]+")/i', "href='$href'", $attributes );
1948
1949 // check if the link is active.
1950 $attributes = $this->add_link_active_class( $href, $attributes );
1951
1952 $preload_link = $this->get_preload_link( $href, $this_data );
1953 }
1954
1955 if ( isset( $this_data['properties']['dynamicContent'] ) ) {
1956 unset( $this_data['properties']['dynamicContent'] );
1957 }
1958 }
1959
1960 $child_content_or_html = $this->get_child_content_or_childrens( $this_data, $options );
1961
1962 $nested_not_allowed_tags = array( 'a', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' );
1963 if ( in_array( $tag, $nested_not_allowed_tags ) && HelperFunctions::check_string_has_this_tags( $child_content_or_html, $tag ) ) {
1964 $tag = 'div';
1965 }
1966 // Start Tag <div>.
1967 $html = '<' . $tag . ' ' . $attributes . '>';
1968 $html .= $child_content_or_html;
1969 $html .= '</' . $tag . '>';
1970 $html .= $preload_link;
1971 // End Tag </div>.
1972 return $html;
1973 }
1974
1975 public function get_child_content_or_childrens( $this_data, $options ) {
1976 $html = '';
1977 if ( ! isset( $this_data['children'] ) ) {
1978 $html .= $this->print_content( $this_data, $options );
1979 } else {
1980 // Check if the current element has children
1981 if ( isset( $this_data['id'], $this->data[ $this_data['id'] ], $this->data[ $this_data['id'] ]['children'] ) ) {
1982 $child_count = count( $this->data[ $this_data['id'] ]['children'] );
1983 for ( $i = 0; $i < $child_count; $i++ ) {
1984 // Position of the child inside its parent. Kept separate from `item_index`,
1985 // which carries the collection item index down the whole item markup.
1986 $merged_options = array_merge(
1987 $options,
1988 array(
1989 'child_index' => $i,
1990 )
1991 );
1992 $html .= $this->recGenHTML( $this->data[ $this_data['id'] ]['children'][ $i ], $merged_options );
1993 }
1994 }
1995 }
1996 return $html;
1997 }
1998
1999 /**
2000 * Print richtext content.
2001 *
2002 * @param array $this_data single element data.
2003 * @param array $options single element options/dynamic data.
2004 * @return string html markup.
2005 */
2006 private function print_content( $this_data, $options ) {
2007 $html = '';
2008 $properties = $this_data['properties'];
2009 $content = isset( $properties['contents'] ) ? $properties['contents'] : '';
2010 $dynamic_content = isset( $properties['dynamicContent'] ) ? $properties['dynamicContent'] : false;
2011 $tag = isset( $this_data['properties'], $this_data['properties']['tag'] ) ? $this_data['properties']['tag'] : 'div';
2012
2013 if ( $dynamic_content ) {
2014 $html = Utils::getDynamicRichTextValue( $dynamic_content, $options );
2015 } else {
2016 if ( is_array( $content ) ) {
2017 $content_count = count( $content );
2018 for ( $i = 0; $i < $content_count; $i++ ) {
2019 if ( is_array( $content[ $i ] ) ) {
2020 if( isset($content[ $i ]['id']) ){
2021 $html .= $this->recGenHTML( $content[ $i ]['id'], $options );
2022 }
2023 } else {
2024 $html .= htmlspecialchars( $content[ $i ] );
2025 }
2026 }
2027 } else {
2028 $html .= htmlspecialchars( $content );
2029 }
2030 }
2031
2032 $href = $this->get_href_value( $this_data, $options );
2033
2034 if ( $tag !== 'a' && $href ) {
2035 $target = isset( $properties['attributes'], $properties['attributes']['target'] ) ? "target={$properties['attributes']['target']}" : '';
2036 $rel = '';
2037
2038 // check if rel is array and convert to string
2039 if (isset($properties['attributes'], $properties['attributes']['rel'])) {
2040 $rel_value = $properties['attributes']['rel'];
2041
2042 if (is_array($rel_value)) {
2043 $rel_value = implode(' ', $rel_value);
2044 }
2045
2046 $rel = "rel='{$rel_value}'";
2047 }
2048
2049 if ( isset( $properties['type'] ) ) {
2050 $html = "<a href='{$href}' {$target} {$rel}>{$html}</a>";
2051 }
2052 }
2053
2054 return $html;
2055 }
2056
2057 public function get_href_value( $this_data, $options = array() ) {
2058 $name = $this_data['name'];
2059 $properties = $this_data['properties'];
2060 $dynamic_content = isset( $properties['dynamicContent'] ) ? $properties['dynamicContent'] : false;
2061 $href = isset( $properties['attributes']['href'] ) ? $properties['attributes']['href'] : false;
2062
2063 switch ( $name ) {
2064 case 'link-block':
2065 case 'link-text':
2066 case 'navigation-item':
2067 case 'paragraph':
2068 case 'heading':
2069 case 'button': {
2070 if ( $dynamic_content ) {
2071
2072 $contentInfo = array(
2073 'dynamicContent' => $dynamic_content,
2074 'options' => $options,
2075 );
2076 if ( isset( $options['itemType'], $options[ $options['itemType'] ], $options[ $options['itemType'] ]->ID ) ) {
2077 $contentInfo['collectionItem'] = array(
2078 'ID' => $options[ $options['itemType'] ]->ID,
2079 );
2080 }
2081 $content = apply_filters( 'kirki_dynamic_content', false, $contentInfo );
2082 if ( $content ) {
2083 // Plain-text dynamic values (comment fields) are visitor supplied,
2084 // so they are never trusted as a URL here.
2085 if ( is_string( $content ) && in_array( $dynamic_content['type'] ?? '', Utils::get_plain_text_dynamic_content_types(), true ) ) {
2086 return esc_url( html_entity_decode( $content, ENT_QUOTES | ENT_HTML5, 'UTF-8' ) );
2087 }
2088 return $content;
2089 }
2090
2091 if ( $dynamic_content['type'] === 'post' ) {
2092 if ( isset( $options['post'], $dynamic_content['value'] ) && isset( $options['post']->{$dynamic_content['value']} ) ) {
2093 $href = $options['post']->{$dynamic_content['value']};
2094 } else {
2095 $href = HelperFunctions::get_post_dynamic_content( isset($dynamic_content['value']) ? $dynamic_content['value'] : false, isset( $options['post'] ) ? $options['post'] : null );
2096 }
2097 } elseif ( $dynamic_content['type'] === 'term' && isset( $options['term'], $options['term']['term_id'] ) ) {
2098
2099 // $href = get_tag_link($options['term']['term_id']);
2100
2101 $term = $options['term'];
2102 $taxonomy = isset( $term['taxonomy'] ) ? $term['taxonomy'] : 'post_tag';
2103
2104 $href = get_term_link( $term['term_id'], $taxonomy );
2105 if ( is_wp_error( $href ) ) {
2106 error_log( 'get_term_link error: ' . $href->get_error_message() );
2107 $href = '';
2108 }
2109 // $href = $options['post']->{$dynamic_content['value']};
2110 } elseif ( $dynamic_content['type'] === 'menu' && isset( $options['menu'], $options['menu']->{$dynamic_content['value']} ) ) {
2111 $href = $options['menu']->{$dynamic_content['value']};
2112 } elseif ( $dynamic_content['type'] === 'user' && isset( $options['user'], $options['user'][ $dynamic_content['value'] ] ) ) {
2113 $href = $options['user'][ $dynamic_content['value'] ];
2114 // $href = $options['post']->{$dynamic_content['value']};
2115 } else {
2116 $itemType = $dynamic_content['type'];
2117 if ( isset( $options[ $itemType ] ) ) {
2118 $data = $options[ $itemType ] ?? array();
2119 $value = isset( $data[ $dynamic_content['value'] ] ) ? $data[ $dynamic_content['value'] ] : '';
2120 $href = $value;
2121 }
2122 }
2123 } elseif ( $dynamic_content && isset( $options['itemType'], $dynamic_content['value'] ) && $options['itemType'] === 'term' ) {
2124 $term = $options['term'];
2125 $property = $dynamic_content['value'];
2126 // Check if 'term' and the dynamic property are set
2127 if ( isset( $term ) && isset( $term->$property ) ) {
2128 // Use the dynamic property's value as the href
2129 $href = $term->$property;
2130 } else {
2131 // Use the fallback link generated by get_tag_link
2132 $href = get_tag_link( $term['term_id'] );
2133 }
2134 $href = HelperFunctions::content_manager_link_filter( $dynamic_content, $href );
2135 } else {
2136 if ( isset( $properties['type'] ) ) {
2137 if ( $properties['type'] === 'popup' ) {
2138 // $href = 'javascript:void(0);';
2139 } elseif ( $properties['type'] === 'page' ) {
2140 $href = isset( $properties['attributes']['href'] ) ? $properties['attributes']['href'] : false;
2141 if ( is_numeric( $href ) && intval( $href ) == $href ) {
2142 // thats means its a page id
2143 $href = get_permalink( $href );
2144 }
2145 } elseif ( $properties['type'] === 'section' ) {
2146 $cleaned_id = ltrim( $href, '#' );
2147 $href = '#' . self::get_unique_section_id_from_title( $cleaned_id );
2148 }
2149 }
2150 }
2151 return $href;
2152 }
2153 default: {
2154 return $href;
2155 }
2156 }
2157 }
2158
2159 public function get_unique_section_id_from_title( $current_id ) {
2160 if ( isset( self::$section_id_tracker_for_href[ $current_id ] ) ) {
2161 return self::$section_id_tracker_for_href[ $current_id ];
2162 }
2163
2164 $section_title = isset( $this->data[ $current_id ]['title'] ) ? $this->data[ $current_id ]['title'] : $current_id;
2165 $base_slug = strtolower( preg_replace( '/\s+/', '-', $section_title ) );
2166 $href = $base_slug;
2167 $suffix = 1;
2168
2169 while ( in_array( $href, self::$section_id_tracker_for_href ) ) {
2170 $href = $base_slug . '-' . $suffix++;
2171 }
2172
2173 self::$section_id_tracker_for_href[ $current_id ] = $href;
2174 return $href;
2175 }
2176
2177 /**
2178 * Get preload link
2179 *
2180 * @param string $href value
2181 * @param array $this_data single element data.
2182 *
2183 * @return string preload link
2184 */
2185 public function get_preload_link( $href, $this_data ) {
2186 $properties = $this_data['properties'];
2187
2188 if ( isset( $properties['preload'] ) && $properties['preload'] !== 'default' ) {
2189 return '<link rel="' . $properties['preload'] . '" href="' . $href . '">';
2190 }
2191
2192 return '';
2193 }
2194
2195 /**
2196 * Get class names
2197 *
2198 * @param string $href value
2199 * @param array $this_data single element data.
2200 * @param string $attributes
2201 *
2202 * @return string updated attributes
2203 */
2204 public function add_link_active_class( $href, $attributes, $this_data = null, $options = null ) {
2205
2206 if ( ! $href ) {
2207 return $attributes;
2208 }
2209
2210 // Resolve $href to a post ID
2211 $targetId = null;
2212 $is_home = false;
2213
2214 // 1. If it's a plain permalink like ?page_id=437
2215 if ( strpos( $href, 'page_id=' ) !== false ) {
2216 $url_parts = wp_parse_url( $href );
2217 if ( ! empty( $url_parts['query'] ) ) {
2218 parse_str( $url_parts['query'], $params );
2219 if ( ! empty( $params['page_id'] ) ) {
2220 $targetId = (int) $params['page_id'];
2221 }
2222 }
2223 } else {
2224 // 2. If it's a relative or full pretty permalink
2225 if ( strpos( $href, 'http' ) !== 0 ) {
2226 $href = home_url( $href );
2227 }
2228
2229 $href = rtrim( $href, '/' );
2230 $home_url = rtrim( home_url(), '/' );
2231
2232 if ( $href === $home_url ) {
2233 $is_home = true;
2234 } else {
2235 $post = get_page_by_path( ltrim( (string) wp_parse_url( $href, PHP_URL_PATH ), '/' ) );
2236
2237 if ( $post ) {
2238 $targetId = $post->ID;
2239 }
2240 }
2241 }
2242
2243 $currentId = HelperFunctions::get_post_id_if_possible_from_url();
2244 $home_page_id = get_option( 'page_on_front' );
2245 $current_term_id = HelperFunctions::get_term_id_if_possible_from_url();
2246 $current_url = strtok( home_url( add_query_arg( null, null ) ), '?' );
2247 $current_url = rtrim( $current_url, '/' );
2248 $term_id = null;
2249
2250 if ( $this_data && $options && isset( $this_data['properties']['dynamicContent'] ) && $this_data['properties']['dynamicContent']['type'] && $this_data['properties']['dynamicContent']['type'] === 'term' && $this_data['properties']['dynamicContent']['value'] === 'link' ) {
2251 $term_id = $options['term']['term_id'];
2252 }
2253
2254 $is_match = (
2255 ( $targetId && $currentId && $targetId === $currentId ) ||
2256 ( $is_home && $currentId && $currentId === intval( $home_page_id ) ) ||
2257 ( $term_id && $current_term_id && $term_id === $current_term_id ) ||
2258 ( $current_url === $href )
2259 );
2260
2261 if ( $is_match ) {
2262 if ( preg_match( '/class="/i', $attributes ) ) {
2263 $attributes = preg_replace(
2264 '/class="([^"]*)"/i',
2265 'class="$1 ' . 'kirki-active-link"',
2266 $attributes
2267 );
2268 } else {
2269 $attributes .= ' class="' . 'kirki-active-link"';
2270 }
2271 }
2272
2273 return $attributes;
2274 }
2275
2276 /**
2277 * Elements for no end tag
2278 *
2279 * @param array $this_data element single data.
2280 * @param array $options element single option data.
2281 *
2282 * @return string html markup.
2283 */
2284 private function noEndTagElements( $this_data, $options ) {
2285 $html = '<' . $this_data['properties']['tag'] . ' ' . $this->getAllAttributes( $this_data ) . '/>';
2286 return $html;
2287 }
2288
2289 /**
2290 * Get elements all attributes
2291 *
2292 * @param array $this_element single element data.
2293 * @param array $filter_condition Attribute filter condition. Empty array returns all attributes.
2294 *
2295 * @return string attributes string.
2296 */
2297 public function getAllAttributes( $this_element, $filter_condition = array() ) {
2298 if ( ! isset( $this_element['properties'] ) ) {
2299 return '';
2300 }
2301
2302 $attr_str = '';
2303 $class_names = trim( $this->getClassNames( $this_element ) );
2304 if ( $class_names ) {
2305 $attr_str .= 'class="' . $class_names . '"';
2306 }
2307
2308 $others_attributes = '';
2309 if ( isset( $this_element['properties']['attributes'] ) ) {
2310 $attributes = $this_element['properties']['attributes'];
2311 $element_name = $this_element['name'];
2312
2313 if ( $this_element['name'] === 'image' && isset( $attributes['width'] ) ) {
2314 unset( $attributes['width'] );
2315 }
2316
2317 $others_attributes = array_map(
2318 function ( $value, $key ) use ( $element_name ) {
2319 if ( in_array( $element_name, $this->prevent_anchor_elements, true ) && in_array( $key, $this->anchor_attrs, true ) ) {
2320 return '';
2321 }
2322
2323 if ( ! $this->attribute_validation( $key, $value ) ) {
2324 return '';
2325 }
2326
2327 if ( is_array( $value ) ) {
2328 $value = implode( ' ', $value );
2329 }
2330
2331 return $key . '="' . $value . '"';
2332 },
2333 array_values( $attributes ),
2334 array_keys( $attributes )
2335 );
2336
2337 $others_attributes = ' ' . implode( ' ', $others_attributes );
2338 }
2339
2340 $custom_attributes = '';
2341 if ( isset( $this_element['properties']['customAttributes'] ) ) {
2342 // phpcs:ignore WordPress.PHP.DisallowShortTernary.Found
2343 $attributes = $this_element['properties']['customAttributes'] ?: array();
2344 $custom_attributes = array_map(
2345 function ( $value, $key ) {
2346 return $key . '="' . $value . '"';
2347 },
2348 array_values( $attributes ),
2349 array_keys( $attributes )
2350 );
2351 $custom_attributes = ' ' . implode( ' ', $custom_attributes );
2352 }
2353
2354 if ( count( $filter_condition ) > 0 ) {
2355 $merged_attributes = array_merge( $this_element['properties']['attributes'], $attributes );
2356 // Merge attributes from other sources.
2357 $merged_attributes['class'] = $class_names;
2358 $merged_attributes['data-kirki'] = $this_element['id'];
2359
2360 if ( isset( $filter_condition['rest'] ) && false === $filter_condition['rest'] ) {
2361 // `rest => false` will pick only the `true` filter attributes.
2362 $filtered_attributes = array_filter(
2363 $merged_attributes,
2364 function ( $at ) use ( $filter_condition ) {
2365 return array_key_exists( $at, $filter_condition ) && true === $filter_condition[ $at ];
2366 },
2367 ARRAY_FILTER_USE_KEY
2368 );
2369 } else {
2370 // By default `rest` will be avaluted to `true`, so it will only remove the `false` attributes.
2371 $filtered_attributes = array_filter(
2372 $merged_attributes,
2373 function ( $at ) use ( $filter_condition ) {
2374 return ! ( array_key_exists( $at, $filter_condition ) && false === $filter_condition[ $at ] );
2375 },
2376 ARRAY_FILTER_USE_KEY
2377 );
2378 }
2379
2380 $attr_str = array_reduce(
2381 array_keys( $filtered_attributes ),
2382 function ( $carry, $key ) use ( $filtered_attributes ) {
2383 return $carry . ' ' . $key . '=' . wp_json_encode( $filtered_attributes[ $key ] );
2384 },
2385 '',
2386 );
2387
2388 } else {
2389 $attr_str .= ' data-kirki="' . $this_element['id'] . '"' . $others_attributes . $custom_attributes;
2390 }
2391
2392 if(!empty($this_element['properties']['textStyleId'])) {
2393 $attr_str .= ' data-text_style="' . $this_element['properties']['textStyleId'] . '"';
2394 }
2395
2396 // $attr_str .= ' data-kirki_name="' . $this_element['name'] . '"';
2397
2398 return $attr_str;
2399 }
2400
2401 /**
2402 * Attribute validation form different element attributes.
2403 *
2404 * @param string $attr name of the attribute.
2405 * @param string $value value of the attribute.
2406 *
2407 * @return bool
2408 */
2409 private function attribute_validation( $attr, $value ) {
2410 if ( $attr === 'multiple' && ! $value ) {
2411 return false;
2412 } elseif ( $attr === 'required' && ! $value ) {
2413 return false;
2414 } elseif ( $attr === 'src' && ! $value ) {
2415 return false;
2416 } elseif ( $attr === 'href' && ! $value ) {
2417 return false;
2418 } elseif ( $attr === 'checked' && ! $value ) {
2419 return false;
2420 }
2421 return true;
2422 }
2423
2424 /**
2425 * Get element class names
2426 *
2427 * @param array $this_element element data.
2428 *
2429 * @return string
2430 */
2431 private function getClassNames( $this_element ) {
2432 $class_array = array();
2433
2434 $style_ids = isset( $this_element['styleIds'] ) ? $this_element['styleIds'] : array();
2435
2436 $manageble_style_ids = isset( $this_element['properties'], $this_element['properties']['classesIds'] ) ? $this_element['properties']['classesIds'] : array();
2437 // add in first in styleids
2438 $style_ids = array_merge($manageble_style_ids, $style_ids);
2439
2440 if ( ! empty( $style_ids ) ) {
2441 $style_ids_count = count( $style_ids );
2442 for ( $i = 0; $i < $style_ids_count; $i++ ) {
2443 $style_id = $style_ids[ $i ];
2444 $s_block = isset( $this->style_blocks[ $style_id ] ) ? $this->style_blocks[ $style_id ] : null;
2445 if ( ! isset( $s_block ) ) {
2446 continue;
2447 }
2448 $this->add_to_only_used_style_blocks( $s_block );
2449 $c_name = $s_block['name'];
2450 if ( is_array( $c_name ) ) {
2451 $c_name = HelperFunctions::add_prefix_to_class_name( $this->prefix, $c_name );
2452 $class_array = array_merge( $class_array, $c_name );
2453 } else {
2454 $class_array[] = HelperFunctions::add_prefix_to_class_name( $this->prefix, $c_name );
2455 }
2456 }
2457 }
2458
2459 $ele_class_names = isset( $this_element['className'] ) ? explode( ' ', $this_element['className'] ) : array();
2460 $class_array = array_merge( $ele_class_names, $class_array );
2461
2462 if ( in_array( $this_element['name'], $this->inline_elements, true ) ) {
2463 array_push( $class_array, 'kirki-inline-element' );
2464 }
2465
2466 // Check if 'kirki-active-link' class is already added then remove it
2467 $class_array = array_filter(
2468 $class_array,
2469 function ( $class ) {
2470 return $class !== 'kirki-active-link';
2471 }
2472 );
2473
2474 return $this->makeClassStringFromArray( $class_array );
2475 }
2476
2477 /**
2478 * Make class string from array
2479 *
2480 * @param array $arr class array.
2481 *
2482 * @return string
2483 */
2484 private function makeClassStringFromArray( $arr ) {
2485 $arr = array_unique( $arr );
2486 $arr = array_map(
2487 function ( $c ) {
2488 return HelperFunctions::get_class_name_from_string( $c );
2489 },
2490 $arr
2491 );
2492 return join( ' ', $arr );
2493 }
2494 }
2495