PluginProbe
Elementor Website Builder – more than just a page builder / 3.6.0-dev1
Elementor Website Builder – more than just a page builder v3.6.0-dev1
4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 4.0.8 4.1.0-beta1 4.1.0-dev1 4.0.7 All 451 releases
elementor / trunk / core / experiments / manager.php

manager.php in Elementor Website Builder – more than just a page builder 3.6.0-dev1, at trunk/core/experiments/manager.php

668 lines 20.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Elementor\Core\Experiments;
3
4 use Elementor\Core\Base\Base_Object;
5 use Elementor\Core\Upgrade\Manager as Upgrade_Manager;
6 use Elementor\Modules\System_Info\Module as System_Info;
7 use Elementor\Plugin;
8 use Elementor\Settings;
9 use Elementor\Tracker;
10 use Elementor\Utils;
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit; // Exit if accessed directly
14 }
15
16 class Manager extends Base_Object {
17
18 const RELEASE_STATUS_DEV = 'dev';
19
20 const RELEASE_STATUS_ALPHA = 'alpha';
21
22 const RELEASE_STATUS_BETA = 'beta';
23
24 const RELEASE_STATUS_RC = 'rc';
25
26 const RELEASE_STATUS_STABLE = 'stable';
27
28 const STATE_DEFAULT = 'default';
29
30 const STATE_ACTIVE = 'active';
31
32 const STATE_INACTIVE = 'inactive';
33
34 private $states;
35
36 private $release_statuses;
37
38 private $features;
39
40 /**
41 * Add Feature
42 *
43 * @since 3.1.0
44 * @access public
45 *
46 * @param array $options {
47 * @type string $name
48 * @type string $title
49 * @type string $description
50 * @type string $release_status
51 * @type string $default
52 * @type callable $on_state_change
53 * }
54 *
55 * @return array|null
56 */
57 public function add_feature( array $options ) {
58 if ( isset( $this->features[ $options['name'] ] ) ) {
59 return null;
60 }
61
62 $default_experimental_data = [
63 'description' => '',
64 'release_status' => self::RELEASE_STATUS_ALPHA,
65 'default' => self::STATE_INACTIVE,
66 'mutable' => true,
67 'new_site' => [
68 'default_active' => false,
69 'always_active' => false,
70 'minimum_installation_version' => null,
71 ],
72 'on_state_change' => null,
73 ];
74
75 $allowed_options = [ 'name', 'title', 'description', 'release_status', 'default', 'mutable', 'new_site', 'on_state_change' ];
76
77 $experimental_data = $this->merge_properties( $default_experimental_data, $options, $allowed_options );
78
79 $new_site = $experimental_data['new_site'];
80
81 if ( $new_site['default_active'] || $new_site['always_active'] ) {
82 $is_new_installation = Upgrade_Manager::install_compare( $new_site['minimum_installation_version'], '>=' );
83
84 if ( $is_new_installation ) {
85 if ( $new_site['always_active'] ) {
86 $experimental_data['state'] = self::STATE_ACTIVE;
87
88 $experimental_data['mutable'] = false;
89 } elseif ( $new_site['default_active'] ) {
90 $experimental_data['default'] = self::STATE_ACTIVE;
91 }
92 }
93 }
94
95 if ( $experimental_data['mutable'] ) {
96 $experimental_data['state'] = $this->get_saved_feature_state( $options['name'] );
97 }
98
99 if ( empty( $experimental_data['state'] ) ) {
100 $experimental_data['state'] = self::STATE_DEFAULT;
101 }
102
103 $this->features[ $options['name'] ] = $experimental_data;
104
105 if ( $experimental_data['mutable'] && is_admin() ) {
106 $feature_option_key = $this->get_feature_option_key( $options['name'] );
107
108 $on_state_change_callback = function( $old_state, $new_state ) use ( $experimental_data ) {
109 $this->on_feature_state_change( $experimental_data, $new_state );
110 };
111
112 add_action( 'add_option_' . $feature_option_key, $on_state_change_callback, 10, 2 );
113 add_action( 'update_option_' . $feature_option_key, $on_state_change_callback, 10, 2 );
114 }
115
116 do_action( 'elementor/experiments/feature-registered', $this, $experimental_data );
117
118 return $experimental_data;
119 }
120
121 /**
122 * Remove Feature
123 *
124 * @since 3.1.0
125 * @access public
126 *
127 * @param string $feature_name
128 */
129 public function remove_feature( $feature_name ) {
130 unset( $this->features[ $feature_name ] );
131 }
132
133 /**
134 * Get Features
135 *
136 * @since 3.1.0
137 * @access public
138 *
139 * @param string $feature_name Optional. Default is null
140 *
141 * @return array|null
142 */
143 public function get_features( $feature_name = null ) {
144 return self::get_items( $this->features, $feature_name );
145 }
146
147 /**
148 * Get Active Features
149 *
150 * @since 3.1.0
151 * @access public
152 *
153 * @return array
154 */
155 public function get_active_features() {
156 return array_filter( $this->features, [ $this, 'is_feature_active' ], ARRAY_FILTER_USE_KEY );
157 }
158
159 /**
160 * Is Feature Active
161 *
162 * @since 3.1.0
163 * @access public
164 *
165 * @param string $feature_name
166 *
167 * @return bool
168 */
169 public function is_feature_active( $feature_name ) {
170 $feature = $this->get_features( $feature_name );
171
172 if ( ! $feature ) {
173 return false;
174 }
175
176 return self::STATE_ACTIVE === $this->get_feature_actual_state( $feature );
177 }
178
179 /**
180 * Set Feature Default State
181 *
182 * @since 3.1.0
183 * @access public
184 *
185 * @param string $feature_name
186 * @param int $default_state
187 */
188 public function set_feature_default_state( $feature_name, $default_state ) {
189 $feature = $this->get_features( $feature_name );
190
191 if ( ! $feature ) {
192 return;
193 }
194
195 $this->features[ $feature_name ]['default'] = $default_state;
196 }
197
198 /**
199 * Get Feature Option Key
200 *
201 * @since 3.1.0
202 * @access public
203 *
204 * @param string $feature_name
205 *
206 * @return string
207 */
208 public function get_feature_option_key( $feature_name ) {
209 return 'elementor_experiment-' . $feature_name;
210 }
211
212 private function add_default_features() {
213 $this->add_feature( [
214 'name' => 'e_dom_optimization',
215 'title' => esc_html__( 'Optimized DOM Output', 'elementor' ),
216 'description' => esc_html__( 'Developers, Please Note! This experiment includes some markup changes. If you\'ve used custom code in Elementor, you might have experienced a snippet of code not running. Turning this experiment off allows you to keep prior Elementor markup output settings, and have that lovely code running again.', 'elementor' )
217 . ' <a href="https://go.elementor.com/wp-dash-legacy-optimized-dom" target="_blank">'
218 . esc_html__( 'Learn More', 'elementor' ) . '</a>',
219 'release_status' => self::RELEASE_STATUS_STABLE,
220 'new_site' => [
221 'default_active' => true,
222 'minimum_installation_version' => '3.1.0-beta',
223 ],
224 ] );
225
226 $this->add_feature( [
227 'name' => 'e_optimized_assets_loading',
228 'title' => esc_html__( 'Improved Asset Loading', 'elementor' ),
229 'description' => esc_html__( 'Please Note! The "Improved Asset Loading" mode reduces the amount of code that is loaded on the page by default. When activated, parts of the infrastructure code will be loaded dynamically, only when needed. Keep in mind that activating this experiment may cause conflicts with incompatible plugins.', 'elementor' )
230 . ' <a href="https://go.elementor.com/wp-dash-improved-asset-loading/" target="_blank">'
231 . esc_html__( 'Learn More', 'elementor' ) . '</a>',
232 'release_status' => self::RELEASE_STATUS_BETA,
233 'new_site' => [
234 'default_active' => true,
235 'minimum_installation_version' => '3.2.0-beta',
236 ],
237 ] );
238
239 $this->add_feature( [
240 'name' => 'e_optimized_css_loading',
241 'title' => esc_html__( 'Improved CSS Loading', 'elementor' ),
242 'description' => esc_html__( 'Please Note! The “Improved CSS Loading” mode reduces the amount of CSS code that is loaded on the page by default. When activated, the CSS code will be loaded, rather inline or in a dedicated file, only when needed. Activating this experiment may cause conflicts with incompatible plugins.', 'elementor' )
243 . ' <a href="https://go.elementor.com/wp-dash-improved-css-loading/" target="_blank">'
244 . esc_html__( 'Learn More', 'elementor' ) . '</a>',
245 'release_status' => self::RELEASE_STATUS_BETA,
246 'new_site' => [
247 'default_active' => true,
248 'minimum_installation_version' => '3.3.0-beta',
249 ],
250 ] );
251
252 $this->add_feature( [
253 'name' => 'e_font_icon_svg',
254 'title' => esc_html__( 'Inline Font Icons', 'elementor' ),
255 'description' => esc_html__( 'The “Inline Font Icons” will render the icons as inline SVG without loading the Font-Awsome and the eicons libraries and its related CSS files and fonts. Learn More.', 'elementor' )
256 . ' <a href="https://go.elementor.com/wp-dash-inline-font-awesome/" target="_blank">'
257 . esc_html__( 'Learn More', 'elementor' ) . '</a>',
258 'release_status' => self::RELEASE_STATUS_ALPHA,
259 ] );
260
261 $this->add_feature( [
262 'name' => 'a11y_improvements',
263 'title' => esc_html__( 'Accessibility Improvements', 'elementor' ),
264 'description' => esc_html__( 'An array of accessibility enhancements in Elementor pages.', 'elementor' )
265 . '<br><strong>' . esc_html__( 'Please note!', 'elementor' ) . '</strong> ' . esc_html__( 'These enhancements may include some markup changes to existing elementor widgets', 'elementor' )
266 . ' <a href="https://go.elementor.com/wp-dash-a11y-improvements" target="_blank">'
267 . esc_html__( 'Learn More', 'elementor' ) . '</a>',
268 'release_status' => self::RELEASE_STATUS_STABLE,
269 'new_site' => [
270 'default_active' => true,
271 'minimum_installation_version' => '3.1.0-beta',
272 ],
273 ] );
274
275 $this->add_feature( [
276 'name' => 'e_import_export',
277 'title' => esc_html__( 'Import Export Template Kit', 'elementor' ),
278 'description' => esc_html__( 'Design sites faster with a template kit that contains some or all components of a complete site, like templates, content & site settings.', 'elementor' )
279 . '<br>'
280 . esc_html__( 'You can import a kit and apply it to your site, or export the elements from this site to be used anywhere else.', 'elementor' ),
281 'release_status' => self::RELEASE_STATUS_STABLE,
282 'default' => self::STATE_ACTIVE,
283 'new_site' => [
284 'default_active' => true,
285 'minimum_installation_version' => '3.2.0-beta',
286 ],
287 ] );
288
289 $this->add_feature( [
290 'name' => 'additional_custom_breakpoints',
291 'title' => esc_html__( 'Additional Custom Breakpoints', 'elementor' ),
292 'description' => esc_html__( 'Get pixel-perfect design for every screen size. You can now add up to 6 customizable breakpoints beyond the default desktop setting: mobile, mobile extra, tablet, tablet extra, laptop, and widescreen.', 'elementor' )
293 . '<br /><strong>' . esc_html__( 'Please note! Conditioning controls on values of responsive controls is not supported when this mode is active.', 'elementor' ) . '</strong>'
294 . ' <a href="https://go.elementor.com/wp-dash-additional-custom-breakpoints/" target="_blank">'
295 . esc_html__( 'Learn More', 'elementor' ) . '</a>',
296 'release_status' => self::RELEASE_STATUS_BETA,
297 'new_site' => [
298 'default_active' => true,
299 'minimum_installation_version' => '3.4.0-beta',
300 ],
301 ] );
302
303 $this->add_feature( [
304 'name' => 'e_hidden_wordpress_widgets',
305 'title' => esc_html__( 'Hide native WordPress widgets from search results', 'elementor' ),
306 'description' => esc_html__( 'WordPress widgets will not be shown when searching in the editor panel. Instead, these widgets can be found in the “WordPress” dropdown at the bottom of the panel.', 'elementor' ),
307 'release_status' => self::RELEASE_STATUS_STABLE,
308 'default' => self::STATE_ACTIVE,
309 ] );
310
311 $this->add_feature( [
312 'name' => 'admin_menu_rearrangement',
313 'mutable' => false,
314 ] );
315
316 $this->add_feature( [
317 'name' => 'container',
318 'title' => esc_html__( 'Container', 'elementor' ),
319 'description' => esc_html__(
320 'Create advanced layouts and responsive designs using the new Container element.
321 When this experiment is active, Containers will be the default building block.
322 Existing Sections, Inner Sections and Columns will not be affected.',
323 'elementor'
324 ),
325 'release_status' => self::RELEASE_STATUS_ALPHA,
326 'default' => self::STATE_INACTIVE,
327 ] );
328 }
329
330 /**
331 * Init States
332 *
333 * @since 3.1.0
334 * @access private
335 */
336 private function init_states() {
337 $this->states = [
338 self::STATE_DEFAULT => esc_html__( 'Default', 'elementor' ),
339 self::STATE_ACTIVE => esc_html__( 'Active', 'elementor' ),
340 self::STATE_INACTIVE => esc_html__( 'Inactive', 'elementor' ),
341 ];
342 }
343
344 /**
345 * Init Statuses
346 *
347 * @since 3.1.0
348 * @access private
349 */
350 private function init_release_statuses() {
351 $this->release_statuses = [
352 self::RELEASE_STATUS_DEV => esc_html__( 'Development', 'elementor' ),
353 self::RELEASE_STATUS_ALPHA => esc_html__( 'Alpha', 'elementor' ),
354 self::RELEASE_STATUS_BETA => esc_html__( 'Beta', 'elementor' ),
355 self::RELEASE_STATUS_RC => esc_html__( 'Release Candidate', 'elementor' ),
356 self::RELEASE_STATUS_STABLE => esc_html__( 'Stable', 'elementor' ),
357 ];
358 }
359
360 /**
361 * Init Features
362 *
363 * @since 3.1.0
364 * @access private
365 */
366 private function init_features() {
367 $this->features = [];
368
369 $this->add_default_features();
370
371 do_action( 'elementor/experiments/default-features-registered', $this );
372 }
373
374 /**
375 * Register Settings Fields
376 *
377 * @param Settings $settings
378 *
379 * @since 3.1.0
380 * @access private
381 *
382 */
383 private function register_settings_fields( Settings $settings ) {
384 $features = $this->get_features();
385
386 $fields = [];
387
388 foreach ( $features as $feature_name => $feature ) {
389 if ( ! $feature['mutable'] ) {
390 unset( $features[ $feature_name ] );
391
392 continue;
393 }
394
395 $feature_key = 'experiment-' . $feature_name;
396
397 $section = 'stable' === $feature['release_status'] ? 'stable' : 'ongoing';
398
399 $fields[ $section ][ $feature_key ]['label'] = $this->get_feature_settings_label_html( $feature );
400
401 $fields[ $section ][ $feature_key ]['field_args'] = $feature;
402
403 $fields[ $section ][ $feature_key ]['render'] = function( $feature ) {
404 $this->render_feature_settings_field( $feature );
405 };
406 }
407
408 foreach ( [ 'stable', 'ongoing' ] as $section ) {
409 if ( ! isset( $fields[ $section ] ) ) {
410 $fields[ $section ]['no_features'] = [
411 'label' => esc_html__( 'No available experiments', 'elementor' ),
412 'field_args' => [
413 'type' => 'raw_html',
414 'html' => esc_html__( 'The current version of Elementor doesn\'t have any experimental features . if you\'re feeling curious make sure to come back in future versions.', 'elementor' ),
415 ],
416 ];
417 }
418
419 if ( ! Tracker::is_allow_track() && 'stable' === $section ) {
420 $fields[ $section ] += $settings->get_usage_fields();
421 }
422 }
423
424 $settings->add_tab(
425 'experiments', [
426 'label' => esc_html__( 'Experiments', 'elementor' ),
427 'sections' => [
428 'ongoing_experiments' => [
429 'callback' => function() {
430 $this->render_settings_intro();
431 },
432 'fields' => $fields['ongoing'],
433 ],
434 'stable_experiments' => [
435 'callback' => function() {
436 $this->render_stable_section_title();
437 },
438 'fields' => $fields['stable'],
439 ],
440 ],
441 ]
442 );
443 }
444
445 private function render_stable_section_title() {
446 ?>
447 <hr>
448 <h2>
449 <?php echo esc_html__( 'Stable Features', 'elementor' ); ?>
450 </h2>
451 <?php
452 }
453
454 /**
455 * Render Settings Intro
456 *
457 * @since 3.1.0
458 * @access private
459 */
460 private function render_settings_intro() {
461 ?>
462 <h2>
463 <?php echo esc_html__( 'Elementor Experiments', 'elementor' ); ?>
464 </h2>
465 <p class="e-experiment__description">
466 <?php
467 printf(
468 /* translators: %1$s Link open tag, %2$s: Link close tag. */
469 esc_html__( 'Access new and experimental features from Elementor before they\'re officially released. As these features are still in development, they are likely to change, evolve or even be removed altogether. %1$sLearn More.%2$s', 'elementor' ),
470 '<a href="https://go.elementor.com/wp-dash-experiments/" target="_blank">',
471 '</a>'
472 );
473 ?>
474 </p>
475 <p class="e-experiment__description">
476 <?php echo esc_html__( 'To use an experiment on your site, simply click on the dropdown next to it and switch to Active. You can always deactivate them at any time.', 'elementor' ); ?>
477 </p>
478 <p class="e-experiment__description">
479 <?php
480 printf(
481 /* translators: %1$s Link open tag, %2$s: Link close tag. */
482 esc_html__( 'Your feedback is important - %1$shelp us%2$s improve these features by sharing your thoughts and inputs.', 'elementor' ),
483 '<a href="https://go.elementor.com/wp-dash-experiments-report-an-issue/" target="_blank">',
484 '</a>'
485 );
486 ?>
487 </p>
488 <?php if ( $this->get_features() ) { ?>
489 <button type="button" class="button e-experiment__button" value="active">Activate All Experiments</button>
490 <button type="button" class="button e-experiment__button" value="inactive">Deactivate All Experiments</button>
491 <?php } ?>
492 <hr>
493 <h2 class="e-experiment__table-title">
494 <?php echo esc_html__( 'Ongoing Experiments', 'elementor' ); ?>
495 </h2>
496 <?php
497 }
498
499 /**
500 * Render Feature Settings Field
501 *
502 * @since 3.1.0
503 * @access private
504 *
505 * @param array $feature
506 */
507 private function render_feature_settings_field( array $feature ) {
508 ?>
509 <div class="e-experiment__content">
510 <select id="e-experiment-<?php echo $feature['name']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" class="e-experiment__select" name="<?php echo $this->get_feature_option_key( $feature['name'] ); ?>">
511 <?php foreach ( $this->states as $state_key => $state_title ) { ?>
512 <option value="<?php echo $state_key; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" <?php selected( $state_key, $feature['state'] ); ?>><?php echo $state_title; ?></option>
513 <?php } ?>
514 </select>
515 <p class="description"><?php echo $feature['description']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></p>
516 <?php if ( 'stable' !== $feature['release_status'] ) { ?>
517 <div class="e-experiment__status"><?php echo sprintf( esc_html__( 'Status: %s', 'elementor' ), $this->release_statuses[ $feature['release_status'] ] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
518 <?php } ?>
519 </div>
520 <?php
521 }
522
523 /**
524 * Get Feature Settings Label HTML
525 *
526 * @since 3.1.0
527 * @access private
528 *
529 * @param array $feature
530 *
531 * @return string
532 */
533 private function get_feature_settings_label_html( array $feature ) {
534 ob_start();
535
536 $is_feature_active = $this->is_feature_active( $feature['name'] );
537
538 $indicator_classes = 'e-experiment__title__indicator';
539
540 if ( $is_feature_active ) {
541 $indicator_classes .= ' e-experiment__title__indicator--active';
542 }
543
544 $indicator_tooltip = $this->get_feature_state_label( $feature );
545
546 ?>
547 <div class="e-experiment__title">
548 <div class="<?php echo $indicator_classes; ?>" data-tooltip="<?php echo $indicator_tooltip; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>"></div>
549 <label class="e-experiment__title__label" for="e-experiment-<?php echo $feature['name']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>"><?php echo $feature['title']; ?></label>
550 </div>
551 <?php
552
553 return ob_get_clean();
554 }
555
556 /**
557 * Get Feature State Label
558 *
559 * @param array $feature
560 *
561 * @return string
562 */
563 public function get_feature_state_label( array $feature ) {
564 $is_feature_active = $this->is_feature_active( $feature['name'] );
565
566 if ( self::STATE_DEFAULT === $feature['state'] ) {
567 $label = $is_feature_active ? esc_html__( 'Active by default', 'elementor' ) :
568 esc_html__( 'Inactive by default', 'elementor' );
569 } else {
570 $label = self::STATE_ACTIVE === $feature['state'] ? esc_html__( 'Active', 'elementor' ) :
571 esc_html__( 'Inactive', 'elementor' );
572 }
573
574 return $label;
575 }
576
577 /**
578 * Get Feature Settings Label HTML
579 *
580 * @since 3.1.0
581 * @access private
582 *
583 * @param string $feature_name
584 *
585 * @return int
586 */
587 private function get_saved_feature_state( $feature_name ) {
588 return get_option( $this->get_feature_option_key( $feature_name ) );
589 }
590
591 /**
592 * Get Feature Actual State
593 *
594 * @since 3.1.0
595 * @access private
596 *
597 * @param array $feature
598 *
599 * @return string
600 */
601 private function get_feature_actual_state( array $feature ) {
602 if ( self::STATE_DEFAULT !== $feature['state'] ) {
603 return $feature['state'];
604 }
605
606 return $feature['default'];
607 }
608
609 /**
610 * On Feature State Change
611 *
612 * @since 3.1.0
613 * @access private
614 *
615 * @param array $old_feature_data
616 * @param string $new_state
617 */
618 private function on_feature_state_change( array $old_feature_data, $new_state ) {
619 $this->features[ $old_feature_data['name'] ]['state'] = $new_state;
620
621 $new_feature_data = $this->get_features( $old_feature_data['name'] );
622
623 $actual_old_state = $this->get_feature_actual_state( $old_feature_data );
624
625 $actual_new_state = $this->get_feature_actual_state( $new_feature_data );
626
627 if ( $actual_old_state === $actual_new_state ) {
628 return;
629 }
630
631 Plugin::$instance->files_manager->clear_cache();
632
633 if ( $new_feature_data['on_state_change'] ) {
634 $new_feature_data['on_state_change']( $actual_old_state, $actual_new_state );
635 }
636 }
637
638 public function __construct() {
639 $this->init_states();
640
641 $this->init_release_statuses();
642
643 $this->init_features();
644
645 add_action( 'admin_init', function () {
646 System_Info::add_report(
647 'experiments', [
648 'file_name' => __DIR__ . '/experiments-reporter.php',
649 'class_name' => __NAMESPACE__ . '\Experiments_Reporter',
650 ]
651 );
652 }, 79 /* Before log */ );
653
654 if ( is_admin() ) {
655 $page_id = Settings::PAGE_ID;
656
657 add_action( "elementor/admin/after_create_settings/{$page_id}", function( Settings $settings ) {
658 $this->register_settings_fields( $settings );
659 }, 11 );
660 }
661
662 // Register CLI commands.
663 if ( Utils::is_wp_cli() ) {
664 \WP_CLI::add_command( 'elementor experiments', WP_CLI::class );
665 }
666 }
667 }
668