Hotel Raxa - Advanced Booking System Implementation

🏨 Hotel Booking Enhancements:
- Implemented Eagle Booking Advanced Pricing add-on
- Added Booking.com-style rate management system
- Created professional calendar interface for pricing
- Integrated deals and discounts functionality

💰 Advanced Pricing Features:
- Dynamic pricing models (per room, per person, per adult)
- Base rates, adult rates, and child rates management
- Length of stay discounts and early bird deals
- Mobile rates and secret deals implementation
- Seasonal promotions and flash sales

📅 Availability Management:
- Real-time availability tracking
- Stop sell and restriction controls
- Closed to arrival/departure functionality
- Minimum/maximum stay requirements
- Automatic sold-out management

💳 Payment Integration:
- Maintained Redsys payment gateway integration
- Seamless integration with existing Eagle Booking
- No modifications to core Eagle Booking plugin

🛠️ Technical Implementation:
- Custom database tables for advanced pricing
- WordPress hooks and filters integration
- AJAX-powered admin interface
- Data migration from existing Eagle Booking
- Professional calendar view for revenue management

📊 Admin Interface:
- Booking.com-style management dashboard
- Visual rate and availability calendar
- Bulk operations for date ranges
- Statistics and analytics dashboard
- Modal dialogs for quick editing

🔧 Code Quality:
- WordPress coding standards compliance
- Secure database operations with prepared statements
- Proper input validation and sanitization
- Error handling and logging
- Responsive admin interface

🤖 Generated with Claude Code (https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Hotel Raxa Dev
2025-07-11 07:43:22 +02:00
commit 5b1e2453c7
9816 changed files with 2784509 additions and 0 deletions

View File

@@ -0,0 +1,143 @@
<?php
namespace Elementor\Modules\AdminBar;
use Elementor\Core\Base\Document;
use Elementor\Core\Base\App as BaseApp;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseApp {
/**
* @var Document[]
*/
private $documents = [];
/**
* @return bool
*/
public static function is_active() {
return is_admin_bar_showing();
}
/**
* @return string
*/
public function get_name() {
return 'admin-bar';
}
/**
* Collect the documents that was rendered in the current page.
*
* @param Document $document
* @param $is_excerpt
*/
public function add_document_to_admin_bar( Document $document, $is_excerpt ) {
if (
$is_excerpt ||
! $document::get_property( 'show_on_admin_bar' ) ||
! $document->is_editable_by_current_user()
) {
return;
}
$this->documents[ $document->get_main_id() ] = $document;
}
/**
* Scripts for module.
*/
public function enqueue_scripts() {
if ( empty( $this->documents ) ) {
return;
}
// Should load 'elementor-admin-bar' before 'admin-bar'
wp_dequeue_script( 'admin-bar' );
wp_enqueue_script(
'elementor-admin-bar',
$this->get_js_assets_url( 'elementor-admin-bar' ),
[ 'elementor-frontend-modules' ],
ELEMENTOR_VERSION,
true
);
// This is a core script of WordPress, it is not required to pass the 'ver' argument.
wp_enqueue_script( // phpcs:ignore WordPress.WP.EnqueuedResourceParameters
'admin-bar',
null,
[ 'elementor-admin-bar' ],
false,
true
);
$this->print_config( 'elementor-admin-bar' );
}
/**
* Creates admin bar menu items config.
*
* @return array
*/
public function get_init_settings() {
$settings = [];
if ( ! empty( $this->documents ) ) {
$settings['elementor_edit_page'] = $this->get_edit_button_config();
}
/**
* Admin bar settings in the frontend.
*
* Register admin_bar config to parse later in the frontend and add to the admin bar with JS.
*
* @since 3.0.0
*
* @param array $settings the admin_bar config
*/
$settings = apply_filters( 'elementor/frontend/admin_bar/settings', $settings );
return $settings;
}
/**
* Creates the config for 'Edit with elementor' menu item.
*
* @return array
*/
private function get_edit_button_config() {
$queried_object_id = get_queried_object_id();
$href = null;
if ( is_singular() && isset( $this->documents[ $queried_object_id ] ) ) {
$href = $this->documents[ $queried_object_id ]->get_edit_url();
unset( $this->documents[ $queried_object_id ] );
}
return [
'id' => 'elementor_edit_page',
'title' => esc_html__( 'Edit with Elementor', 'elementor' ),
'href' => $href,
'children' => array_map( function ( $document ) {
return [
'id' => "elementor_edit_doc_{$document->get_main_id()}",
'title' => $document->get_post()->post_title,
'sub_title' => $document::get_title(),
'href' => $document->get_edit_url(),
];
}, $this->documents ),
];
}
/**
* Module constructor.
*/
public function __construct() {
add_action( 'elementor/frontend/before_get_builder_content', [ $this, 'add_document_to_admin_bar' ], 10, 2 );
add_action( 'wp_footer', [ $this, 'enqueue_scripts' ], 11 /* after third party scripts */ );
}
}

View File

@@ -0,0 +1,150 @@
<?php
namespace Elementor\Modules\AdminTopBar;
use Elementor\Core\Utils\Promotions\Filtered_Promotions_Manager;
use Elementor\Plugin;
use Elementor\Core\Base\App as BaseApp;
use Elementor\Core\Experiments\Manager;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseApp {
/**
* @return bool
*/
public static function is_active() {
return is_admin();
}
/**
* @return string
*/
public function get_name() {
return 'admin-top-bar';
}
private function render_admin_top_bar() {
?>
<div id="e-admin-top-bar-root">
</div>
<?php
}
/**
* Enqueue admin scripts
*/
private function enqueue_scripts() {
wp_enqueue_style( 'elementor-admin-top-bar-fonts', 'https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap', [], ELEMENTOR_VERSION );
wp_enqueue_style( 'elementor-admin-top-bar', $this->get_css_assets_url( 'admin-top-bar', null, 'default', true ), [], ELEMENTOR_VERSION );
/**
* Before admin top bar enqueue scripts.
*
* Fires before Elementor admin top bar scripts are enqueued.
*
* @since 3.19.0
*/
do_action( 'elementor/admin_top_bar/before_enqueue_scripts', $this );
wp_enqueue_script( 'elementor-admin-top-bar', $this->get_js_assets_url( 'admin-top-bar' ), [
'elementor-common',
'react',
'react-dom',
'tipsy',
], ELEMENTOR_VERSION, true );
wp_set_script_translations( 'elementor-admin-top-bar', 'elementor' );
$min_suffix = Utils::is_script_debug() ? '' : '.min';
wp_enqueue_script( 'tipsy', ELEMENTOR_ASSETS_URL . 'lib/tipsy/tipsy' . $min_suffix . '.js', [
'jquery',
], '1.0.0', true );
$this->print_config();
}
private function add_frontend_settings() {
$settings = [];
$settings['is_administrator'] = current_user_can( 'manage_options' );
// TODO: Find a better way to add apps page url to the admin top bar.
$settings['apps_url'] = admin_url( 'admin.php?page=elementor-apps' );
$settings['promotion'] = [
'text' => __( 'Upgrade Now', 'elementor' ),
'url' => 'https://go.elementor.com/wp-dash-admin-top-bar-upgrade/',
];
$settings['promotion'] = Filtered_Promotions_Manager::get_filtered_promotion_data(
$settings['promotion'],
'elementor/admin_top_bar/go_pro_promotion',
'url'
);
$current_screen = get_current_screen();
/** @var \Elementor\Core\Common\Modules\Connect\Apps\Library $library */
$library = Plugin::$instance->common->get_component( 'connect' )->get_app( 'library' );
if ( $library ) {
$settings = array_merge( $settings, [
'is_user_connected' => $library->is_connected(),
'connect_url' => $library->get_admin_url( 'authorize', [
'utm_source' => 'top-bar',
'utm_medium' => 'wp-dash',
'utm_campaign' => 'connect-account',
'utm_content' => $current_screen->id,
'source' => 'generic',
] ),
] );
}
$this->set_settings( $settings );
do_action( 'elementor/admin-top-bar/init', $this );
}
private function is_top_bar_active() {
$current_screen = get_current_screen();
if ( ! $current_screen ) {
return false;
}
$is_elementor_page = strpos( $current_screen->id ?? '', 'elementor' ) !== false;
$is_elementor_post_type_page = strpos( $current_screen->post_type ?? '', 'elementor' ) !== false;
return apply_filters(
'elementor/admin-top-bar/is-active',
$is_elementor_page || $is_elementor_post_type_page,
$current_screen
);
}
/**
* Module constructor.
*/
public function __construct() {
parent::__construct();
add_action( 'current_screen', function () {
if ( ! $this->is_top_bar_active() ) {
return;
}
$this->add_frontend_settings();
add_action( 'in_admin_header', function () {
$this->render_admin_top_bar();
} );
add_action( 'admin_enqueue_scripts', function () {
$this->enqueue_scripts();
} );
} );
}
}

View File

@@ -0,0 +1,790 @@
<?php
namespace Elementor\Modules\Ai\Connect;
use Elementor\Core\Common\Modules\Connect\Apps\Library;
use Elementor\Modules\Ai\Module;
use Elementor\Utils as ElementorUtils;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Ai extends Library {
const API_URL = 'https://my.elementor.com/api/v2/ai/';
const STYLE_PRESET = 'style_preset';
const IMAGE_TYPE = 'image_type';
const IMAGE_STRENGTH = 'image_strength';
const ASPECT_RATIO = 'ratio';
const IMAGE_RESOLUTION = 'image_resolution';
const PROMPT = 'prompt';
public function get_title() {
return esc_html__( 'AI', 'elementor' );
}
protected function get_api_url() {
return static::API_URL . '/';
}
public function get_usage() {
return $this->ai_request(
'POST',
'status/check',
[
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
]
);
}
public function get_cached_usage() {
$cache_key = 'elementor_ai_usage';
$cache_time = 24 * HOUR_IN_SECONDS;
$usage = get_site_transient( $cache_key );
if ( ! $usage ) {
$usage = $this->get_usage();
set_site_transient( $cache_key, $usage, $cache_time );
}
return $usage;
}
public function get_remote_config() {
return $this->ai_request(
'GET',
'remote-config/config',
[
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
]
);
}
public function get_remote_frontend_config( $data ) {
return $this->ai_request(
'POST',
'remote-config/frontend-config',
[
'client_name' => $data['payload']['client_name'],
'client_version' => $data['payload']['client_version'],
'client_session_id' => $data['payload']['client_session_id'],
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
],
false,
'',
'json'
);
}
/**
* get_file_payload
* @param $filename
* @param $file_type
* @param $file_path
* @param $boundary
*
* @return string
*/
private function get_file_payload( $filename, $file_type, $file_path, $boundary ) {
$name = $filename ?? basename( $file_path );
$mine_type = 'image' === $file_type ? image_type_to_mime_type( exif_imagetype( $file_path ) ) : $file_type;
$payload = '';
// Upload the file
$payload .= '--' . $boundary;
$payload .= "\r\n";
$payload .= 'Content-Disposition: form-data; name="' . esc_attr( $name ) . '"; filename="' . esc_attr( $name ) . '"' . "\r\n";
$payload .= 'Content-Type: ' . $mine_type . "\r\n";
$payload .= "\r\n";
$payload .= file_get_contents( $file_path );
$payload .= "\r\n";
return $payload;
}
private function get_upload_request_body( $body, $file, $boundary, $file_name = '' ) {
$payload = '';
// add all body fields as standard POST fields:
foreach ( $body as $name => $value ) {
$payload .= '--' . $boundary;
$payload .= "\r\n";
$payload .= 'Content-Disposition: form-data; name="' . esc_attr( $name ) . '"' . "\r\n\r\n";
$payload .= $value;
$payload .= "\r\n";
}
if ( is_array( $file ) ) {
foreach ( $file as $key => $file_data ) {
$payload .= $this->get_file_payload( $file_data['name'], $file_data['type'], $file_data['path'], $boundary );
}
} else {
$image_mime = image_type_to_mime_type( exif_imagetype( $file ) );
// @todo: add validation for supported image types
if ( empty( $file_name ) ) {
$file_name = basename( $file );
}
$payload .= $this->get_file_payload( $file_name, $image_mime, $file, $boundary );
}
$payload .= '--' . $boundary . '--';
return $payload;
}
private function ai_request( $method, $endpoint, $body, $file = false, $file_name = '', $format = 'default' ) {
$headers = [
'x-elementor-ai-version' => '2',
];
if ( $file ) {
$boundary = wp_generate_password( 24, false );
$body = $this->get_upload_request_body( $body, $file, $boundary, $file_name );
// add content type header
$headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;
} elseif ( 'json' === $format ) {
$headers['Content-Type'] = 'application/json';
$body = wp_json_encode( $body );
}
return $this->http_request(
$method,
$endpoint,
[
'timeout' => 100,
'headers' => $headers,
'body' => $body,
],
[
'return_type' => static::HTTP_RETURN_TYPE_ARRAY,
'with_error_data' => true,
]
);
}
public function set_get_started() {
return $this->ai_request(
'POST',
'status/get-started',
[
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
]
);
}
public function set_status_feedback( $response_id ) {
return $this->ai_request(
'POST',
'status/feedback/' . $response_id,
[
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
]
);
}
public function set_used_gallery_image( $image_id ) {
return $this->ai_request(
'POST',
'status/used-gallery-image/' . $image_id,
[
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
]
);
}
public function get_completion_text( $prompt, $context, $request_ids ) {
return $this->ai_request(
'POST',
'text/completion',
[
'prompt' => $prompt,
'context' => wp_json_encode( $context ),
'ids' => $request_ids,
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
],
false,
'',
'json'
);
}
public function get_excerpt( $prompt, $context, $request_ids ) {
$excerpt_length = apply_filters( 'excerpt_length', 55 );
return $this->ai_request(
'POST',
'text/get-excerpt',
[
'content' => $prompt,
'maxLength' => $excerpt_length,
'context' => wp_json_encode( $context ),
'ids' => $request_ids,
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
],
false,
'',
'json'
);
}
/**
* get_image_prompt_enhanced
* @param $prompt
*
* @return mixed|\WP_Error
*/
public function get_image_prompt_enhanced( $prompt, $context, $request_ids ) {
return $this->ai_request(
'POST',
'text/enhance-image-prompt',
[
'prompt' => $prompt,
'context' => wp_json_encode( $context ),
'ids' => $request_ids,
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
]
);
}
public function get_edit_text( $data, $context, $request_ids ) {
return $this->ai_request(
'POST',
'text/edit',
[
'input' => $data['payload']['input'],
'instruction' => $data['payload']['instruction'],
'context' => wp_json_encode( $context ),
'ids' => $request_ids,
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
],
false,
'',
'json'
);
}
public function get_custom_code( $data, $context, $request_ids ) {
return $this->ai_request(
'POST',
'text/custom-code',
[
'prompt' => $data['payload']['prompt'],
'language' => $data['payload']['language'],
'context' => wp_json_encode( $context ),
'ids' => $request_ids,
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
],
false,
'',
'json'
);
}
public function get_custom_css( $data, $context, $request_ids ) {
return $this->ai_request(
'POST',
'text/custom-css',
[
'prompt' => $data['payload']['prompt'],
'html_markup' => $data['payload']['html_markup'],
'element_id' => $data['payload']['element_id'],
'context' => wp_json_encode( $context ),
'ids' => $request_ids,
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
],
false,
'',
'json'
);
}
/**
* get_text_to_image
* @param $prompt
* @param $prompt_settings
*
* @return mixed|\WP_Error
*/
public function get_text_to_image( $data, $context, $request_ids ) {
return $this->ai_request(
'POST',
'image/text-to-image',
[
self::PROMPT => $data['payload']['prompt'],
self::IMAGE_TYPE => $data['payload']['settings'][ self::IMAGE_TYPE ] . '/' . $data['payload']['settings'][ self::STYLE_PRESET ],
self::ASPECT_RATIO => $data['payload']['settings'][ self::ASPECT_RATIO ],
'context' => wp_json_encode( $context ),
'ids' => $request_ids,
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
],
false,
'',
'json'
);
}
/**
* get_featured_image
* @param $prompt
* @param $prompt_settings
*
* @return mixed|\WP_Error
*/
public function get_featured_image( $data, $context, $request_ids ) {
return $this->ai_request(
'POST',
'image/text-to-image/featured-image',
[
self::PROMPT => $data['payload']['prompt'],
self::IMAGE_TYPE => $data['payload']['settings'][ self::IMAGE_TYPE ] . '/' . $data['payload']['settings'][ self::STYLE_PRESET ],
self::ASPECT_RATIO => $data['payload']['settings'][ self::ASPECT_RATIO ],
'context' => wp_json_encode( $context ),
'ids' => $request_ids,
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
],
false,
'',
'json'
);
}
/**
* get_image_to_image
* @param $image_data
*
* @return mixed|\WP_Error
* @throws \Exception
*/
public function get_image_to_image( $image_data, $context, $request_ids ) {
$image_file = get_attached_file( $image_data['attachment_id'] );
if ( ! $image_file ) {
throw new \Exception( 'Image file not found' );
}
$result = $this->ai_request(
'POST',
'image/image-to-image',
[
self::PROMPT => $image_data[ self::PROMPT ],
self::IMAGE_TYPE => $image_data['promptSettings'][ self::IMAGE_TYPE ] . '/' . $image_data['promptSettings'][ self::STYLE_PRESET ],
self::IMAGE_STRENGTH => $image_data['promptSettings'][ self::IMAGE_STRENGTH ],
self::ASPECT_RATIO => $image_data['promptSettings'][ self::ASPECT_RATIO ],
'context' => wp_json_encode( $context ),
'ids' => $request_ids,
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
],
$image_file,
'image'
);
return $result;
}
/**
* get_image_to_image_upscale
* @param $image_data
*
* @return mixed|\WP_Error
* @throws \Exception
*/
public function get_image_to_image_upscale( $image_data, $context, $request_ids ) {
$image_file = get_attached_file( $image_data['attachment_id'] );
if ( ! $image_file ) {
throw new \Exception( 'Image file not found' );
}
$result = $this->ai_request(
'POST',
'image/image-to-image/upscale',
[
self::IMAGE_RESOLUTION => $image_data['promptSettings']['upscale_to'],
'context' => wp_json_encode( $context ),
'ids' => $request_ids,
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
],
$image_file,
'image'
);
return $result;
}
/**
* get_image_to_image_remove_background
* @param $image_data
*
* @return mixed|\WP_Error
* @throws \Exception
*/
public function get_image_to_image_remove_background( $image_data, $context, $request_ids ) {
$image_file = get_attached_file( $image_data['attachment_id'] );
if ( ! $image_file ) {
throw new \Exception( 'Image file not found' );
}
$result = $this->ai_request(
'POST',
'image/image-to-image/remove-background',
[
'context' => wp_json_encode( $context ),
'ids' => $request_ids,
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
],
$image_file,
'image'
);
return $result;
}
/**
* get_image_to_image_remove_text
* @param $image_data
*
* @return mixed|\WP_Error
* @throws \Exception
*/
public function get_image_to_image_replace_background( $image_data, $context, $request_ids ) {
$image_file = get_attached_file( $image_data['attachment_id'] );
if ( ! $image_file ) {
throw new \Exception( 'Image file not found' );
}
$result = $this->ai_request(
'POST',
'image/image-to-image/replace-background',
[
self::PROMPT => $image_data[ self::PROMPT ],
'context' => wp_json_encode( $context ),
'ids' => $request_ids,
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
],
$image_file,
'image'
);
return $result;
}
/**
* store_temp_file
* used to store a temp file for the AI request and deletes it once the request is done
* @param $file_content
* @param $file_ext
*
* @return string
*/
private function store_temp_file( $file_content, $file_ext = '' ) {
$temp_file = str_replace( '.tmp', '', wp_tempnam() . $file_ext );
file_put_contents( $temp_file, $file_content );
// make sure the temp file is deleted on shutdown
register_shutdown_function( function () use ( $temp_file ) {
if ( file_exists( $temp_file ) ) {
unlink( $temp_file );
}
} );
return $temp_file;
}
/**
* get_image_to_image_out_painting
* @param $image_data
*
* @return mixed|\WP_Error
* @throws \Exception
*/
public function get_image_to_image_out_painting( $image_data, $context, $request_ids ) {
$img_content = str_replace( ' ', '+', $image_data['mask'] );
$img_content = substr( $img_content, strpos( $img_content, ',' ) + 1 );
$img_content = base64_decode( $img_content );
$mask_file = $this->store_temp_file( $img_content, '.png' );
if ( ! $mask_file ) {
throw new \Exception( 'Expended Image file not found' );
}
$result = $this->ai_request(
'POST',
'image/image-to-image/outpainting',
[
'context' => wp_json_encode( $context ),
'ids' => $request_ids,
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
'size' => wp_json_encode( $image_data['size'] ),
'position' => wp_json_encode( $image_data['position'] ),
'image_base64' => $image_data['image_base64'],
$image_data['image'],
],
[
[
'name' => 'image',
'type' => 'image',
'path' => $mask_file,
],
]
);
return $result;
}
/**
* get_image_to_image_mask
* @param $image_data
*
* @return mixed|\WP_Error
* @throws \Exception
*/
public function get_image_to_image_mask( $image_data, $context, $request_ids ) {
$image_file = get_attached_file( $image_data['attachment_id'] );
$mask_file = $this->store_temp_file( $image_data['mask'], '.svg' );
if ( ! $image_file ) {
throw new \Exception( 'Image file not found' );
}
if ( ! $mask_file ) {
throw new \Exception( 'Mask file not found' );
}
$result = $this->ai_request(
'POST',
'image/image-to-image/inpainting',
[
self::PROMPT => $image_data[ self::PROMPT ],
'context' => wp_json_encode( $context ),
'ids' => $request_ids,
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
'image_base64' => $image_data['image_base64'],
],
[
[
'name' => 'image',
'type' => 'image',
'path' => $image_file,
],
[
'name' => 'mask_image',
'type' => 'image/svg+xml',
'path' => $mask_file,
],
]
);
return $result;
}
public function get_image_to_image_mask_cleanup( $image_data, $context, $request_ids ) {
$image_file = get_attached_file( $image_data['attachment_id'] );
$mask_file = $this->store_temp_file( $image_data['mask'], '.svg' );
if ( ! $image_file ) {
throw new \Exception( 'Image file not found' );
}
if ( ! $mask_file ) {
throw new \Exception( 'Mask file not found' );
}
$result = $this->ai_request(
'POST',
'image/image-to-image/cleanup',
[
self::PROMPT => $image_data[ self::PROMPT ],
'context' => wp_json_encode( $context ),
'ids' => $request_ids,
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
'image_base64' => $image_data['image_base64'],
],
[
[
'name' => 'image',
'type' => 'image',
'path' => $image_file,
],
[
'name' => 'mask_image',
'type' => 'image/svg+xml',
'path' => $mask_file,
],
]
);
return $result;
}
public function generate_layout( $data, $context ) {
$endpoint = 'generate/layout';
$body = [
'prompt' => $data['prompt'],
'variationType' => (int) $data['variationType'],
'ids' => $data['ids'],
];
if ( ! empty( $data['prevGeneratedIds'] ) ) {
$body['generatedBaseTemplatesIds'] = $data['prevGeneratedIds'];
}
if ( ! empty( $data['attachments'] ) ) {
$attachment = $data['attachments'][0];
switch ( $attachment['type'] ) {
case 'json':
$endpoint = 'generate/generate-json-variation';
$body['json'] = [
'type' => 'elementor',
'elements' => [ $attachment['content'] ],
'label' => $attachment['label'],
'source' => $attachment['source'],
];
break;
case 'url':
$endpoint = 'generate/html-to-elementor';
$html = wp_json_encode( $attachment['content'] );
$body['html'] = $html;
$body['htmlFetchedUrl'] = $attachment['label'];
break;
}
}
$context['currentContext'] = $data['currentContext'];
$context['features'] = [
'supportedFeatures' => [ 'Taxonomy' ],
];
if ( ElementorUtils::has_pro() ) {
$context['features']['subscriptions'] = [ 'Pro' ];
}
if ( Plugin::$instance->experiments->is_feature_active( 'container_grid' ) ) {
$context['features']['supportedFeatures'][] = 'Grid';
}
if ( Plugin::instance()->experiments->get_active_features()['nested-elements'] ) {
$context['features']['supportedFeatures'][] = 'Nested';
}
if ( Plugin::instance()->experiments->get_active_features()['mega-menu'] ) {
$context['features']['supportedFeatures'][] = 'MegaMenu';
}
if ( class_exists( 'WC' ) ) {
$context['features']['supportedFeatures'][] = 'WooCommerce';
}
$metadata = [
'context' => $context,
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
'config' => [
'generate' => [
'all' => true,
],
],
];
$body = array_merge( $body, $metadata );
// Temp hack for platforms that filters the http_request_args, and it breaks JSON requests.
remove_all_filters( 'http_request_args' );
return $this->ai_request(
'POST',
$endpoint,
$body,
false,
'',
'json'
);
}
public function get_layout_prompt_enhanced( $prompt, $enhance_type, $context ) {
return $this->ai_request(
'POST',
'generate/enhance-prompt',
[
'prompt' => $prompt,
'enhance_type' => $enhance_type,
'context' => wp_json_encode( $context ),
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
],
false,
'',
'json'
);
}
public function get_history_by_type( $type, $page, $limit, $context = [] ) {
$endpoint = Module::HISTORY_TYPE_ALL === $type
? 'history'
: add_query_arg( [
'page' => $page,
'limit' => $limit,
], "history/{$type}" );
return $this->ai_request(
'POST',
$endpoint,
[
'context' => wp_json_encode( $context ),
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
]
);
}
public function delete_history_item( $id, $context = [] ) {
return $this->ai_request(
'DELETE', 'history/' . $id,
[
'context' => wp_json_encode( $context ),
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
]
);
}
public function toggle_favorite_history_item( $id, $context = [] ) {
return $this->ai_request(
'POST', sprintf( 'history/%s/favorite', $id ),
[
'context' => wp_json_encode( $context ),
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
]
);
}
protected function init() {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,113 @@
<?php
namespace Elementor\Modules\Ai;
use Elementor\User;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Preferences {
const ENABLE_AI = 'elementor_enable_ai';
/**
* Register actions and hooks.
*
* @return void
*/
public function register() {
add_action( 'personal_options', function ( \WP_User $user ) {
$this->add_personal_options_settings( $user );
} );
add_action( 'personal_options_update', function ( $user_id ) {
$this->update_personal_options_settings( $user_id );
} );
add_action( 'edit_user_profile_update', function ( $user_id ) {
$this->update_personal_options_settings( $user_id );
} );
}
/**
* Determine if AI features are enabled for a user.
*
* @param int $user_id - User ID.
*
* @return bool
*/
public static function is_ai_enabled( $user_id ) {
return (bool) User::get_user_option_with_default( static::ENABLE_AI, $user_id, true );
}
/**
* Add settings to the "Personal Options".
*
* @param \WP_User $user - User object.
*
* @return void
*/
protected function add_personal_options_settings( \WP_User $user ) {
if ( ! $this->has_permissions_to_edit_user( $user->ID ) ) {
return;
}
$ai_value = User::get_user_option_with_default( static::ENABLE_AI, $user->ID, '1' );
?>
<tr>
<th style="padding:0px">
<h2><?php echo esc_html__( 'Elementor - AI', 'elementor' ); ?></h2>
</th>
</tr>
<tr>
<th>
<label for="<?php echo esc_attr( static::ENABLE_AI ); ?>">
<?php echo esc_html__( 'Status', 'elementor' ); ?>
</label>
</th>
<td>
<label for="<?php echo esc_attr( static::ENABLE_AI ); ?>">
<input name="<?php echo esc_attr( static::ENABLE_AI ); ?>" id="<?php echo esc_attr( static::ENABLE_AI ); ?>" type="checkbox" value="1"<?php checked( '1', $ai_value ); ?> />
<?php echo esc_html__( 'Enable Elementor AI functionality', 'elementor' ); ?>
</label>
</td>
</tr>
<?php
}
/**
* Save the settings in the "Personal Options".
*
* @param int $user_id - User ID.
*
* @return void
*/
protected function update_personal_options_settings( $user_id ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce already verified in `wp_verify_nonce`.
$wpnonce = Utils::get_super_global_value( $_POST, '_wpnonce' );
if ( ! wp_verify_nonce( $wpnonce, 'update-user_' . $user_id ) ) {
return;
}
if ( ! $this->has_permissions_to_edit_user( $user_id ) ) {
return;
}
$ai_value = empty( $_POST[ static::ENABLE_AI ] ) ? '0' : '1';
update_user_option( $user_id, static::ENABLE_AI, sanitize_text_field( $ai_value ) );
}
/**
* Determine if the current user has permission to view/change preferences of a user.
*
* @param int $user_id
*
* @return bool
*/
protected function has_permissions_to_edit_user( $user_id ) {
return current_user_can( 'edit_user', $user_id );
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Elementor\Modules\Announcements\Classes;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Announcement {
/**
* @var array
*/
protected $raw_data;
/**
* @var array
*/
protected $triggers;
public function __construct( array $data ) {
$this->raw_data = $data;
$this->set_triggers();
}
/**
* @return array
*/
protected function get_triggers(): array {
return $this->triggers;
}
protected function set_triggers() {
$triggers = $this->raw_data['triggers'] ?? [];
foreach ( $triggers as $trigger ) {
$this->triggers[] = Utils::get_trigger_object( $trigger );
}
}
/**
* is_active
* @return bool
*/
public function is_active(): bool {
$triggers = $this->get_triggers();
if ( empty( $triggers ) ) {
return true;
}
foreach ( $triggers as $trigger ) {
if ( ! $trigger->is_active() ) {
return false;
}
}
return true;
}
public function after_triggered() {
foreach ( $this->get_triggers() as $trigger ) {
if ( $trigger->is_active() ) {
$trigger->after_triggered();
}
}
}
/**
* @return array
*/
public function get_prepared_data(): array {
$raw_data = $this->raw_data;
unset( $raw_data['triggers'] );
return $raw_data;
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Elementor\Modules\Announcements\Classes;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Trigger_Base {
/**
* @var string
*/
protected $name = 'trigger-base';
/**
* @return string
*/
public function get_name(): string {
return $this->name;
}
/**
* @return bool
*/
public function is_active(): bool {
return true;
}
public function after_triggered() {
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Elementor\Modules\Announcements\Classes;
use Elementor\Modules\Announcements\Triggers\{
IsFlexContainerInactive, AiStarted
};
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Utils {
/**
* get_trigger_object
*
* @param $trigger
*
* @return IsFlexContainerInactive|false
*/
public static function get_trigger_object( $trigger ) {
$object_trigger = apply_filters( 'elementor/announcements/trigger_object', false, $trigger );
if ( false !== $object_trigger ) {
return $object_trigger;
}
//@TODO - replace with trigger manager
switch ( $trigger['action'] ) {
case 'isFlexContainerInactive':
return new IsFlexContainerInactive();
case 'aiStarted':
return new AiStarted();
default:
return false;
}
}
}

View File

@@ -0,0 +1,184 @@
<?php
namespace Elementor\Modules\Announcements;
use Elementor\Core\Base\App as BaseApp;
use Elementor\Modules\Ai\Preferences;
use Elementor\Modules\Announcements\Classes\Announcement;
use Elementor\Settings as ElementorSettings;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseApp {
/**
* @return bool
*/
public static function is_active(): bool {
return is_admin();
}
/**
* @return string
*/
public function get_name(): string {
return 'announcements';
}
/**
* Render wrapper for the app to load.
*/
private function render_app_wrapper() {
?>
<div id="e-announcements-root"></div>
<?php
}
/**
* Enqueue app scripts.
*/
private function enqueue_scripts() {
wp_enqueue_script(
'announcements-app',
$this->get_js_assets_url( 'announcements-app' ),
[
'wp-i18n',
],
ELEMENTOR_VERSION,
true
);
wp_set_script_translations( 'announcements-app', 'elementor' );
$this->print_config( 'announcements-app' );
}
/**
* Get initialization settings to use in frontend.
*
* @return array[]
*/
protected function get_init_settings(): array {
$active_announcements = $this->get_active_announcements();
$additional_settings = [];
foreach ( $active_announcements as $announcement ) {
$additional_settings[] = $announcement->get_prepared_data();
//@TODO - replace with ajax request from the front after actually triggered
$announcement->after_triggered();
}
return [
'announcements' => $additional_settings,
];
}
/**
* Enqueue the module styles.
*/
public function enqueue_styles() {
wp_enqueue_style(
'announcements-app',
$this->get_css_assets_url( 'modules/announcements/announcements' ),
[],
ELEMENTOR_VERSION
);
}
/**
* Retrieve all announcement in raw format ( array ).
*
* @return array[]
*/
private function get_raw_announcements(): array {
$raw_announcements = [];
if ( Preferences::is_ai_enabled( get_current_user_id() ) ) {
$raw_announcements[] = $this->get_ai_announcement_data();
}
// DO NOT USE THIS FILTER
return apply_filters( 'elementor/announcements/raw_announcements', $raw_announcements );
}
private function get_ai_announcement_data(): array {
return [
'title' => __( 'Discover your new superpowers ', 'elementor' ),
'description' => __( '<p>With AI for text, code, image generation and editing, you can bring your vision to life faster than ever. Start your free trial now - <b>no credit card required!</b></p>', 'elementor' ),
'media' => [
'type' => 'image',
'src' => ELEMENTOR_ASSETS_URL . 'images/announcement.png?' . ELEMENTOR_VERSION,
],
'cta' => [
[
'label' => __( 'Let\'s do it', 'elementor' ),
'variant' => 'primary',
'target' => '_top',
'url' => '#welcome-ai',
],
[
'label' => __( 'Skip', 'elementor' ),
'variant' => 'secondary',
],
],
'triggers' => [
[
'action' => 'aiStarted',
],
],
];
}
/**
* Retrieve all announcement objects.
*
* @return array
*/
private function get_announcements(): array {
$announcements = [];
foreach ( $this->get_raw_announcements() as $announcement_data ) {
$announcements[] = new Announcement( $announcement_data );
}
return $announcements;
}
/**
* Retrieve all active announcement objects.
*
* @return array
*/
private function get_active_announcements(): array {
$active_announcements = [];
foreach ( $this->get_announcements() as $announcement ) {
if ( $announcement->is_active() ) {
$active_announcements[] = $announcement;
}
}
return $active_announcements;
}
public function __construct() {
parent::__construct();
add_action( 'elementor/init', [ $this, 'on_elementor_init' ] );
}
public function on_elementor_init() {
if ( empty( $this->get_active_announcements() ) ) {
return;
}
add_action( 'elementor/editor/footer', function () {
$this->render_app_wrapper();
} );
add_action( 'elementor/editor/after_enqueue_scripts', function () {
$this->enqueue_scripts();
$this->enqueue_styles();
} );
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Elementor\Modules\Announcements\Triggers;
use Elementor\Modules\Announcements\Classes\Trigger_Base;
use Elementor\User;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class AiStarted extends Trigger_Base {
/**
* @var string
*/
protected $name = 'ai-get-started-announcement';
public function after_triggered() {
User::set_introduction_viewed( [ 'introductionKey' => $this->name ] );
}
/**
* @return bool
*/
public function is_active(): bool {
return ! User::get_introduction_meta( 'ai_get_started' ) && ! User::get_introduction_meta( $this->name );
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Elementor\Modules\Announcements\Triggers;
use Elementor\Modules\Announcements\Classes\Trigger_Base;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class IsFlexContainerInactive extends Trigger_Base {
const USER_META_KEY = 'announcements_user_counter';
/**
* @var string
*/
protected $name = 'is-flex-container-inactive';
/**
* @return int
*/
protected function get_view_count(): int {
$user_counter = $this->get_user_announcement_count();
return ! empty( $user_counter ) ? (int) $user_counter : 0;
}
public function after_triggered() {
$new_counter = $this->get_view_count() + 1;
update_user_meta( get_current_user_id(), self::USER_META_KEY, $new_counter );
}
/**
* @return bool
*/
public function is_active(): bool {
$is_feature_active = Plugin::$instance->experiments->is_feature_active( 'container' );
$counter = $this->get_user_announcement_count();
return ! $is_feature_active && (int) $counter < 1;
}
/**
* @return string
*/
private function get_user_announcement_count(): string {
return get_user_meta( get_current_user_id(), self::USER_META_KEY, true );
}
}

View File

@@ -0,0 +1,191 @@
<?php
namespace Elementor\Modules\Apps;
use Elementor\Core\Isolation\Wordpress_Adapter;
use Elementor\Core\Isolation\Plugin_Status_Adapter;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Admin_Apps_Page {
const APPS_URL = 'https://assets.elementor.com/apps/v1/apps.json';
private static ?Wordpress_Adapter $wordpress_adapter = null;
private static ?Plugin_Status_Adapter $plugin_status_adapter = null;
public static function render() {
?>
<div class="wrap e-a-apps">
<div class="e-a-page-title">
<h2><?php echo esc_html__( 'Popular Add-ons, New Possibilities.', 'elementor' ); ?></h2>
<p><?php echo esc_html__( 'Boost your web-creation process with add-ons, plugins, and more tools specially selected to unleash your creativity, increase productivity, and enhance your Elementor-powered website.', 'elementor' ); ?>*<br>
<a href="https://go.elementor.com/wp-dash-apps-about-apps-page/" target="_blank"><?php echo esc_html__( 'Learn more about this page.', 'elementor' ); ?></a>
</p>
</div>
<div class="e-a-list">
<?php self::render_plugins_list(); ?>
</div>
<div class="e-a-page-footer">
<p>*<?php echo esc_html__( 'Please note that certain tools and services on this page are developed by third-party companies and are not part of Elementor\'s suite of products or support. Before using them, we recommend independently evaluating them. Additionally, when clicking on their action buttons, you may be redirected to an external website.', 'elementor' ); ?></p>
</div>
</div>
<?php
}
private static function render_plugins_list() {
$plugins = self::get_plugins();
foreach ( $plugins as $plugin ) {
self::render_plugin_item( $plugin );
}
}
private static function get_plugins() : array {
if ( ! self::$wordpress_adapter ) {
self::$wordpress_adapter = new Wordpress_Adapter();
}
if ( ! self::$plugin_status_adapter ) {
self::$plugin_status_adapter = new Plugin_Status_Adapter( self::$wordpress_adapter );
}
$apps = static::get_remote_apps();
return static::filter_apps( $apps );
}
private static function get_remote_apps() {
$apps = wp_remote_get( static::APPS_URL );
if ( is_wp_error( $apps ) ) {
return [];
}
$apps = json_decode( wp_remote_retrieve_body( $apps ), true );
if ( empty( $apps['apps'] ) || ! is_array( $apps['apps'] ) ) {
return [];
}
return $apps['apps'];
}
private static function filter_apps( $apps ) {
$filtered_apps = [];
foreach ( $apps as $app ) {
if ( static::is_wporg_app( $app ) ) {
$app = static::filter_wporg_app( $app );
}
if ( static::is_ecom_app( $app ) ) {
$app = static::filter_ecom_app( $app );
}
if ( empty( $app ) ) {
continue;
}
$filtered_apps[] = $app;
}
return $filtered_apps;
}
private static function is_wporg_app( $app ) {
return isset( $app['type'] ) && 'wporg' === $app['type'];
}
private static function filter_wporg_app( $app ) {
if ( self::$wordpress_adapter->is_plugin_active( $app['file_path'] ) ) {
return null;
}
if ( self::$plugin_status_adapter->is_plugin_installed( $app['file_path'] ) ) {
if ( current_user_can( 'activate_plugins' ) ) {
$app['action_label'] = esc_html__( 'Activate', 'elementor' );
$app['action_url'] = self::$plugin_status_adapter->get_activate_plugin_url( $app['file_path'] );
} else {
$app['action_label'] = esc_html__( 'Cannot Activate', 'elementor' );
$app['action_url'] = '#';
}
} else {
if ( current_user_can( 'install_plugins' ) ) {
$app['action_label'] = esc_html__( 'Install', 'elementor' );
$app['action_url'] = self::$plugin_status_adapter->get_install_plugin_url( $app['file_path'] );
} else {
$app['action_label'] = esc_html__( 'Cannot Install', 'elementor' );
$app['action_url'] = '#';
}
}
return $app;
}
private static function is_ecom_app( $app ) {
return isset( $app['type'] ) && 'ecom' === $app['type'];
}
private static function filter_ecom_app( $app ) {
if ( self::$wordpress_adapter->is_plugin_active( $app['file_path'] ) ) {
return null;
}
if ( ! self::$plugin_status_adapter->is_plugin_installed( $app['file_path'] ) ) {
return $app;
}
if ( current_user_can( 'activate_plugins' ) ) {
$app['action_label'] = esc_html__( 'Activate', 'elementor' );
$app['action_url'] = self::$plugin_status_adapter->get_activate_plugin_url( $app['file_path'] );
} else {
$app['action_label'] = esc_html__( 'Cannot Activate', 'elementor' );
$app['action_url'] = '#';
}
$app['target'] = '_self';
return $app;
}
private static function get_images_url() {
return ELEMENTOR_URL . 'modules/apps/images/';
}
private static function is_elementor_pro_installed() {
return defined( 'ELEMENTOR_PRO_VERSION' );
}
private static function render_plugin_item( $plugin ) {
?>
<div class="e-a-item"<?php echo ! empty( $plugin['file_path'] ) ? ' data-plugin="' . esc_attr( $plugin['file_path'] ) . '"' : ''; ?>>
<div class="e-a-heading">
<img class="e-a-img" src="<?php echo esc_url( $plugin['image'] ); ?>" alt="<?php echo esc_attr( $plugin['name'] ); ?>">
<?php if ( ! empty( $plugin['badge'] ) ) : ?>
<span class="e-a-badge"><?php echo esc_html( $plugin['badge'] ); ?></span>
<?php endif; ?>
</div>
<h3 class="e-a-title"><?php echo esc_html( $plugin['name'] ); ?></h3>
<p class="e-a-author"><?php esc_html_e( 'By', 'elementor' ); ?> <a href="<?php echo esc_url( $plugin['author_url'] ); ?>" target="_blank"><?php echo esc_html( $plugin['author'] ); ?></a></p>
<div class="e-a-desc">
<p><?php echo esc_html( $plugin['description'] ); ?></p>
<?php if ( ! empty( $plugin['offering'] ) ) : ?>
<p class="e-a-offering"><?php echo esc_html( $plugin['offering'] ); ?></p>
<?php endif; ?>
</div>
<p class="e-a-actions">
<?php if ( ! empty( $plugin['learn_more_url'] ) ) : ?>
<a class="e-a-learn-more" href="<?php echo esc_url( $plugin['learn_more_url'] ); ?>" target="_blank"><?php echo esc_html__( 'Learn More', 'elementor' ); ?></a>
<?php endif; ?>
<a href="<?php echo esc_url( $plugin['action_url'] ); ?>" class="e-btn e-accent" target="<?php echo isset( $plugin['target'] ) ? esc_attr( $plugin['target'] ) : '_blank'; ?>"><?php echo esc_html( $plugin['action_label'] ); ?></a>
</p>
</div>
<?php
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Elementor\Modules\Apps;
use Elementor\Core\Admin\Menu\Interfaces\Admin_Menu_Item_With_Page;
use Elementor\Settings;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Admin_Menu_Apps implements Admin_Menu_Item_With_Page {
public function is_visible() {
return true;
}
public function get_parent_slug() {
return Settings::PAGE_ID;
}
public function get_label() {
return esc_html__( 'Add-ons', 'elementor' );
}
public function get_page_title() {
return esc_html__( 'Add-ons', 'elementor' );
}
public function get_capability() {
return 'manage_options';
}
public function render() {
Admin_Apps_Page::render();
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Elementor\Modules\Apps;
use Elementor\Core\Upgrade\Manager as Upgrade_Manager;
use Elementor\User;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Admin_Pointer {
const RELEASE_VERSION = '3.15.0';
const CURRENT_POINTER_SLUG = 'e-apps';
public static function add_hooks() {
add_action( 'admin_print_footer_scripts-index.php', [ __CLASS__, 'admin_print_script' ] );
}
public static function admin_print_script() {
if ( static::is_dismissed() || static::is_new_installation() ) {
return;
}
wp_enqueue_script( 'wp-pointer' );
wp_enqueue_style( 'wp-pointer' );
$pointer_content = '<h3>' . esc_html__( 'New! Popular Add-ons', 'elementor' ) . '</h3>';
$pointer_content .= '<p>' . esc_html__( 'Discover our collection of plugins and add-ons carefully selected to enhance your Elementor website and unleash your creativity.', 'elementor' ) . '</p>';
$pointer_content .= sprintf(
'<p><a class="button button-primary" href="%s">%s</a></p>',
admin_url( 'admin.php?page=' . Module::PAGE_ID ),
esc_html__( 'Explore Add-ons', 'elementor' )
)
?>
<script>
jQuery( document ).ready( function( $ ) {
$( '#toplevel_page_elementor' ).pointer( {
content: '<?php echo $pointer_content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>',
position: {
edge: <?php echo is_rtl() ? "'right'" : "'left'"; ?>,
align: 'center'
},
close: function() {
elementorCommon.ajax.addRequest( 'introduction_viewed', {
data: {
introductionKey: '<?php echo esc_attr( static::CURRENT_POINTER_SLUG ); ?>',
},
} );
}
} ).pointer( 'open' );
} );
</script>
<?php
}
private static function is_dismissed() {
return User::get_introduction_meta( static::CURRENT_POINTER_SLUG );
}
private static function is_new_installation() {
return Upgrade_Manager::install_compare( static::RELEASE_VERSION, '>=' );
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Elementor\Modules\Apps;
use Elementor\Core\Admin\Menu\Admin_Menu_Manager;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Settings;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseModule {
const PAGE_ID = 'elementor-apps';
public function get_name() {
return 'apps';
}
public function __construct() {
parent::__construct();
Admin_Pointer::add_hooks();
add_action( 'elementor/admin/menu/register', function( Admin_Menu_Manager $admin_menu ) {
$admin_menu->register( static::PAGE_ID, new Admin_Menu_Apps() );
}, 115 );
add_action( 'elementor/admin/menu/after_register', function ( Admin_Menu_Manager $admin_menu, array $hooks ) {
if ( ! empty( $hooks[ static::PAGE_ID ] ) ) {
add_action( "admin_print_scripts-{$hooks[ static::PAGE_ID ]}", [ $this, 'enqueue_assets' ] );
}
}, 10, 2 );
add_filter( 'elementor/finder/categories', function( array $categories ) {
$categories['site']['items']['apps'] = [
'title' => esc_html__( 'Add-ons', 'elementor' ),
'url' => admin_url( 'admin.php?page=' . static::PAGE_ID ),
'icon' => 'apps',
'keywords' => [ 'apps', 'addon', 'plugin', 'extension', 'integration' ],
];
return $categories;
} );
// Add the Elementor Apps link to the plugin install action links.
add_filter( 'install_plugins_tabs', [ $this, 'add_elementor_plugin_install_action_link' ] );
add_action( 'install_plugins_pre_elementor', [ $this, 'maybe_open_elementor_tab' ] );
add_action( 'admin_print_styles-plugin-install.php', [ $this, 'add_plugins_page_styles' ] );
}
public function enqueue_assets() {
add_filter( 'admin_body_class', [ $this, 'body_status_classes' ] );
wp_enqueue_style(
'elementor-apps',
$this->get_css_assets_url( 'modules/apps/admin' ),
[],
ELEMENTOR_VERSION
);
}
public function body_status_classes( $admin_body_classes ) {
$admin_body_classes .= ' elementor-apps-page';
return $admin_body_classes;
}
public function add_elementor_plugin_install_action_link( $tabs ) {
$tabs['elementor'] = esc_html__( 'For Elementor', 'elementor' );
return $tabs;
}
public function maybe_open_elementor_tab() {
if ( ! isset( $_GET['tab'] ) || 'elementor' !== $_GET['tab'] ) {
return;
}
$elementor_url = add_query_arg( [
'page' => static::PAGE_ID,
'tab' => 'elementor',
'ref' => 'plugins',
], admin_url( 'admin.php' ) );
wp_safe_redirect( $elementor_url );
exit;
}
public function add_plugins_page_styles() {
?>
<style>
.plugin-install-elementor > a::after {
content: "";
display: inline-block;
background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M8.33321 3H12.9999V7.66667H11.9999V4.70711L8.02009 8.68689L7.31299 7.97978L11.2928 4H8.33321V3Z' fill='%23646970'/%3E%3Cpath d='M6.33333 4.1665H4.33333C3.8731 4.1665 3.5 4.5396 3.5 4.99984V11.6665C3.5 12.1267 3.8731 12.4998 4.33333 12.4998H11C11.4602 12.4998 11.8333 12.1267 11.8333 11.6665V9.6665' stroke='%23646970'/%3E%3C/svg%3E%0A");
width: 16px;
height: 16px;
background-repeat: no-repeat;
vertical-align: text-top;
margin-left: 2px;
}
.plugin-install-elementor:hover > a::after {
background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M8.33321 3H12.9999V7.66667H11.9999V4.70711L8.02009 8.68689L7.31299 7.97978L11.2928 4H8.33321V3Z' fill='%23135E96'/%3E%3Cpath d='M6.33333 4.1665H4.33333C3.8731 4.1665 3.5 4.5396 3.5 4.99984V11.6665C3.5 12.1267 3.8731 12.4998 4.33333 12.4998H11C11.4602 12.4998 11.8333 12.1267 11.8333 11.6665V9.6665' stroke='%23135E96'/%3E%3C/svg%3E%0A");
}
</style>
<?php
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Base;
use JsonSerializable;
abstract class Atomic_Control_Base implements JsonSerializable {
private string $bind;
private $label = null;
private $description = null;
abstract public function get_type(): string;
abstract public function get_props(): array;
public static function bind_to( string $prop_name ) {
return new static( $prop_name );
}
protected function __construct( string $prop_name ) {
$this->bind = $prop_name;
}
public function get_bind() {
return $this->bind;
}
public function set_label( string $label ): self {
$this->label = $label;
return $this;
}
public function set_description( string $description ): self {
$this->description = $description;
return $this;
}
public function jsonSerialize(): array {
return [
'type' => 'control',
'value' => [
'type' => $this->get_type(),
'bind' => $this->get_bind(),
'label' => $this->label,
'description' => $this->description,
'props' => $this->get_props(),
],
];
}
}

View File

@@ -0,0 +1,161 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Base;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Widget_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Atomic_Widget_Base extends Widget_Base {
protected $version = '0.0';
protected $styles = [];
public function __construct( $data = [], $args = null ) {
parent::__construct( $data, $args );
$this->version = $data['version'] ?? '0.0';
$this->styles = $data['styles'] ?? [];
}
public function get_atomic_controls() {
$controls = $this->define_atomic_controls();
return $this->get_valid_controls( $controls );
}
private function get_valid_controls( array $controls ): array {
$valid_controls = [];
$schema = static::get_props_schema();
foreach ( $controls as $control ) {
if ( $control instanceof Section ) {
$cloned_section = clone $control;
$cloned_section->set_items(
$this->get_valid_controls( $control->get_items() )
);
$valid_controls[] = $cloned_section;
continue;
}
if ( ! ( $control instanceof Atomic_Control_Base ) ) {
$this->safe_throw( 'Control must be an instance of `Atomic_Control_Base`.' );
continue;
}
$prop_name = $control->get_bind();
if ( ! $prop_name ) {
$this->safe_throw( 'Control is missing a bound prop from the schema.' );
continue;
}
if ( ! array_key_exists( $prop_name, $schema ) ) {
$this->safe_throw( "Prop `{$prop_name}` is not defined in the schema of `{$this->get_name()}`. Did you forget to define it?" );
continue;
}
$valid_controls[] = $control;
}
return $valid_controls;
}
private function safe_throw( string $message ) {
if ( ! defined( 'ELEMENTOR_DEBUG' ) || ! ELEMENTOR_DEBUG ) {
return;
}
throw new \Exception( $message );
}
abstract protected function define_atomic_controls(): array;
final public function get_controls( $control_id = null ) {
if ( ! empty( $control_id ) ) {
return null;
}
return [];
}
final public function get_initial_config() {
$config = parent::get_initial_config();
$config['atomic_controls'] = $this->get_atomic_controls();
$config['version'] = $this->version;
return $config;
}
final public function get_data_for_save() {
$data = parent::get_data_for_save();
$data['version'] = $this->version;
return $data;
}
final public function get_raw_data( $with_html_content = false ) {
$raw_data = parent::get_raw_data( $with_html_content );
$raw_data['styles'] = $this->styles;
return $raw_data;
}
final public function get_stack( $with_common_controls = true ) {
return [
'controls' => [],
'tabs' => [],
];
}
final public function get_atomic_settings(): array {
$schema = static::get_props_schema();
$raw_settings = $this->get_settings();
$transformed_settings = [];
foreach ( $schema as $key => $prop ) {
if ( array_key_exists( $key, $raw_settings ) ) {
$transformed_settings[ $key ] = $raw_settings[ $key ];
} else {
$transformed_settings[ $key ] = $prop->get_default();
}
$transformed_settings[ $key ] = $this->transform_setting( $transformed_settings[ $key ] );
}
return $transformed_settings;
}
public static function get_props_schema() {
return static::define_props_schema();
}
private function transform_setting( $setting ) {
if ( ! $this->is_transformable( $setting ) ) {
return $setting;
}
switch ( $setting['$$type'] ) {
case 'classes':
return is_array( $setting['value'] )
? join( ' ', $setting['value'] )
: '';
default:
return null;
}
}
private function is_transformable( $setting ): bool {
return ! empty( $setting['$$type'] ) && 'string' === getType( $setting['$$type'] ) && isset( $setting['value'] );
}
abstract protected static function define_props_schema(): array;
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls;
use JsonSerializable;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Section implements JsonSerializable {
private $label = null;
private $description = null;
private array $items = [];
public static function make(): self {
return new static();
}
public function set_label( string $label ): self {
$this->label = $label;
return $this;
}
public function set_description( string $description ): self {
$this->description = $description;
return $this;
}
public function set_items( array $items ): self {
$this->items = $items;
return $this;
}
public function get_items() {
return $this->items;
}
public function jsonSerialize(): array {
return [
'type' => 'section',
'value' => [
'label' => $this->label,
'description' => $this->description,
'items' => $this->items,
],
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls\Types;
use Elementor\Modules\AtomicWidgets\Base\Atomic_Control_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Select_Control extends Atomic_Control_Base {
private array $options = [];
public function get_type(): string {
return 'select';
}
public function set_options( array $options ): self {
$this->options = $options;
return $this;
}
public function get_props(): array {
return [
'options' => $this->options,
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls\Types;
use Elementor\Modules\AtomicWidgets\Base\Atomic_Control_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Textarea_Control extends Atomic_Control_Base {
private $placeholder = null;
public function get_type(): string {
return 'textarea';
}
public function set_placeholder( string $placeholder ): self {
$this->placeholder = $placeholder;
return $this;
}
public function get_props(): array {
return [
'placeholder' => $this->placeholder,
];
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Elementor\Modules\AtomicWidgets;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Experiments\Manager as Experiments_Manager;
use Elementor\Modules\AtomicWidgets\Widgets\Atomic_Heading;
use Elementor\Modules\AtomicWidgets\Widgets\Atomic_Image;
use Elementor\Plugin;
use Elementor\Widgets_Manager;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseModule {
const EXPERIMENT_NAME = 'atomic_widgets';
const PACKAGES = [
'editor-documents', // TODO: NEED to be removed once the editor will not be dependent on the documents package.
'editor-panels',
'editor-editing-panel',
'editor-style',
];
public function get_name() {
return 'atomic-widgets';
}
public function __construct() {
parent::__construct();
$this->register_experiment();
if ( Plugin::$instance->experiments->is_feature_active( self::EXPERIMENT_NAME ) ) {
add_filter( 'elementor/editor/v2/packages', fn( $packages ) => $this->add_packages( $packages ) );
add_filter( 'elementor/widgets/register', fn( Widgets_Manager $widgets_manager ) => $this->register_widgets( $widgets_manager ) );
add_action( 'elementor/editor/after_enqueue_scripts', fn() => $this->enqueue_scripts() );
}
}
private function register_experiment() {
Plugin::$instance->experiments->add_feature( [
'name' => self::EXPERIMENT_NAME,
'title' => esc_html__( 'Atomic Widgets', 'elementor' ),
'description' => esc_html__( 'Enable atomic widgets.', 'elementor' ),
'hidden' => true,
'default' => Experiments_Manager::STATE_INACTIVE,
'release_status' => Experiments_Manager::RELEASE_STATUS_ALPHA,
] );
}
private function add_packages( $packages ) {
return array_merge( $packages, self::PACKAGES );
}
private function register_widgets( Widgets_Manager $widgets_manager ) {
$widgets_manager->register( new Atomic_Heading() );
$widgets_manager->register( new Atomic_Image() );
}
/**
* Enqueue the module scripts.
*
* @return void
*/
private function enqueue_scripts() {
wp_enqueue_script(
'elementor-atomic-widgets-editor',
$this->get_js_assets_url( 'atomic-widgets-editor' ),
[ 'elementor-editor' ],
ELEMENTOR_VERSION,
true
);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Schema;
use JsonSerializable;
class Atomic_Prop implements JsonSerializable {
private $default_value = null;
public static function make(): self {
return new self();
}
public function default( $default_value ): self {
$this->default_value = $default_value;
return $this;
}
public function get_default() {
return $this->default_value;
}
public function jsonSerialize(): array {
return [
'default' => $this->default_value,
];
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Widgets;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Modules\AtomicWidgets\Controls\Types\Select_Control;
use Elementor\Modules\AtomicWidgets\Controls\Types\Textarea_Control;
use Elementor\Modules\AtomicWidgets\Base\Atomic_Widget_Base;
use Elementor\Modules\AtomicWidgets\Schema\Atomic_Prop;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Atomic_Heading extends Atomic_Widget_Base {
public function get_icon() {
return 'eicon-t-letter';
}
public function get_title() {
return esc_html__( 'Atomic Heading', 'elementor' );
}
public function get_name() {
return 'a-heading';
}
protected function render() {
$settings = $this->get_atomic_settings();
// TODO: Move the validation/sanitization to the props schema constraints.
$escaped_tag = Utils::validate_html_tag( $settings['tag'] );
$escaped_title = esc_html( $settings['title'] );
$class = '';
if ( ! empty( $settings['classes'] ) ) {
$class = "class='" . esc_attr( $settings['classes'] ) . "'";
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo "<$escaped_tag $class>$escaped_title</$escaped_tag>";
}
protected function define_atomic_controls(): array {
$tag_control = Select_Control::bind_to( 'tag' )
->set_label( esc_html__( 'Tag', 'elementor' ) )
->set_options( [
[
'value' => 'h1',
'label' => 'H1',
],
[
'value' => 'h2',
'label' => 'H2',
],
[
'value' => 'h3',
'label' => 'H3',
],
[
'value' => 'h4',
'label' => 'H4',
],
[
'value' => 'h5',
'label' => 'H5',
],
[
'value' => 'h6',
'label' => 'H6',
],
]);
$title_control = Textarea_Control::bind_to( 'title' )
->set_label( __( 'Title', 'elementor' ) )
->set_placeholder( __( 'Type your title here', 'elementor' ) );
$tag_and_title_section = Section::make()
->set_label( __( 'Content', 'elementor' ) )
->set_items( [
$tag_control,
$title_control,
]);
return [
$tag_and_title_section,
];
}
protected static function define_props_schema(): array {
return [
'classes' => Atomic_Prop::make(),
'tag' => Atomic_Prop::make()
->default( 'h2' ),
'title' => Atomic_Prop::make()
->default( __( 'Your Title Here', 'elementor' ) ),
];
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Widgets;
use Elementor\Utils;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Modules\AtomicWidgets\Controls\Types\Select_Control;
use Elementor\Modules\AtomicWidgets\Base\Atomic_Widget_Base;
use Elementor\Modules\AtomicWidgets\Schema\Atomic_Prop;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Atomic_Image extends Atomic_Widget_Base {
public function get_icon() {
return 'eicon-image';
}
public function get_title() {
return esc_html__( 'Atomic Image', 'elementor' );
}
public function get_name() {
return 'a-image';
}
protected function render() {
$settings = $this->get_atomic_settings();
// TODO: Replace with actual URL prop
$image_url = $settings['url'];
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo "<img src='" . esc_url( $image_url ) . "' />";
}
private function get_image_size_options() {
$wp_image_sizes = self::get_wp_image_sizes();
$image_sizes = [];
foreach ( $wp_image_sizes as $size_key => $size_attributes ) {
$control_title = ucwords( str_replace( '_', ' ', $size_key ) );
if ( is_array( $size_attributes ) ) {
$control_title .= sprintf( ' - %d*%d', $size_attributes['width'], $size_attributes['height'] );
}
$image_sizes[] = [
'label' => $control_title,
'value' => $size_key,
];
}
$image_sizes[] = [
'label' => esc_html__( 'Full', 'elementor' ),
'value' => 'full',
];
return $image_sizes;
}
private static function get_wp_image_sizes() {
$default_image_sizes = get_intermediate_image_sizes();
$additional_sizes = wp_get_additional_image_sizes();
$image_sizes = [];
foreach ( $default_image_sizes as $size ) {
$image_sizes[ $size ] = [
'width' => (int) get_option( $size . '_size_w' ),
'height' => (int) get_option( $size . '_size_h' ),
'crop' => (bool) get_option( $size . '_crop' ),
];
}
if ( $additional_sizes ) {
$image_sizes = array_merge( $image_sizes, $additional_sizes );
}
// /** This filter is documented in wp-admin/includes/media.php */
return apply_filters( 'image_size_names_choose', $image_sizes );
}
protected function define_atomic_controls(): array {
$options = $this->get_image_size_options();
$resolution_control = Select_Control::bind_to( 'image_size' )
->set_label( esc_html__( 'Image Resolution', 'elementor' ) )
->set_options( $options );
$content_section = Section::make()
->set_label( esc_html__( 'Content', 'elementor' ) )
->set_items( [
$resolution_control,
]);
return [
$content_section,
];
}
protected static function define_props_schema(): array {
return [
'image_size' => Atomic_Prop::make()
->default( 'large' ),
'url' => Atomic_Prop::make()
->default( Utils::get_placeholder_image_src() ),
];
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Elementor\Modules\Checklist;
use Elementor\Core\Isolation\Wordpress_Adapter;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
interface Checklist_Module_Interface {
public function get_name() : string;
public function is_experiment_active() : bool;
public function get_user_progress_from_db() : array;
public function get_step_progress( $step_id ) : ?array;
public function set_step_progress( $step_id, $step_progress ) : void;
public function get_steps_manager() : Steps_Manager;
public function get_wordpress_adapter() : Wordpress_Adapter;
}

View File

@@ -0,0 +1,169 @@
<?php
namespace Elementor\Modules\Checklist;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Experiments\Manager;
use Elementor\Core\Isolation\Wordpress_Adapter;
use Elementor\Core\Isolation\Wordpress_Adapter_Interface;
use Elementor\Plugin;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseModule implements Checklist_Module_Interface {
const EXPERIMENT_ID = 'launchpad-checklist';
const DB_OPTION_KEY = 'elementor_checklist';
private $user_progress = null;
private Steps_Manager $steps_manager;
private Wordpress_Adapter_Interface $wordpress_adapter;
/**
* @param ?Wordpress_Adapter_Interface $wordpress_adapter
*
* @return void
*/
public function __construct( ?Wordpress_Adapter_Interface $wordpress_adapter = null ) {
$this->wordpress_adapter = $wordpress_adapter ?? new Wordpress_Adapter();
parent::__construct();
$this->register_experiment();
if ( ! $this->is_experiment_active() ) {
return;
}
$this->init_user_progress();
$this->user_progress = $this->user_progress ?? $this->get_user_progress_from_db();
$this->steps_manager = new Steps_Manager( $this );
$this->enqueue_editor_scripts();
}
/**
* Get the module name.
*
* @return string
*/
public function get_name() : string {
return 'e-checklist';
}
/**
* Checks if the experiment is active
*
* @return bool
*/
public function is_experiment_active() : bool {
return Plugin::$instance->experiments->is_feature_active( self::EXPERIMENT_ID );
}
/**
* Gets user's progress from db
*
* @return array {
* @type bool $is_hidden
* @type int $last_opened_timestamp
* @type array $steps {
* @type string $step_id => {
* @type bool $is_marked_completed
* @type bool $is_completed
* }
* }
* }
*/
public function get_user_progress_from_db() : array {
return json_decode( $this->wordpress_adapter->get_option( self::DB_OPTION_KEY ), true );
}
/**
* Using the step's ID, get the progress of the step should it exist
*
* @param $step_id
*
* @return null|array {
* @type bool $is_marked_completed
* @type bool $is_completed
* }
*/
public function get_step_progress( $step_id ) : ?array {
return $this->user_progress['steps'][ $step_id ] ?? null;
}
/**
* Update the progress of a step
*
* @param $step_id
* @param $step_progress
*
* @return void
*/
public function set_step_progress( $step_id, $step_progress ) : void {
$this->user_progress['steps'][ $step_id ] = $step_progress;
$this->update_user_progress_in_db();
}
/**
* @return Steps_Manager
*/
public function get_steps_manager() : Steps_Manager {
return $this->steps_manager;
}
/**
* @return Wordpress_Adapter
*/
public function get_wordpress_adapter() : Wordpress_Adapter {
return $this->wordpress_adapter;
}
public function enqueue_editor_scripts() : void {
add_action( 'elementor/editor/before_enqueue_scripts', function () {
$min_suffix = Utils::is_script_debug() ? '' : '.min';
wp_enqueue_script(
$this->get_name(),
ELEMENTOR_ASSETS_URL . 'js/checklist' . $min_suffix . '.js',
[
'react',
'react-dom',
'elementor-common',
'elementor-v2-ui',
'elementor-v2-icons',
'elementor-v2-editor-app-bar',
'elementor-web-cli',
],
ELEMENTOR_VERSION,
true
);
wp_set_script_translations( $this->get_name(), 'elementor' );
} );
}
private function register_experiment() : void {
Plugin::$instance->experiments->add_feature( [
'name' => self::EXPERIMENT_ID,
'title' => esc_html__( 'Launchpad Checklist', 'elementor' ),
'description' => esc_html__( 'Launchpad Checklist feature to boost productivity and deliver your site faster', 'elementor' ),
'release_status' => Manager::RELEASE_STATUS_ALPHA,
'hidden' => true,
] );
}
private function init_user_progress() : void {
$default_settings = [
'is_hidden' => false,
'last_opened_timestamp' => time(),
'steps' => [],
];
$this->wordpress_adapter->add_option( self::DB_OPTION_KEY, wp_json_encode( $default_settings ) );
}
private function update_user_progress_in_db() : void {
$this->wordpress_adapter->update_option( self::DB_OPTION_KEY, wp_json_encode( $this->user_progress ) );
}
}

View File

@@ -0,0 +1,163 @@
<?php
namespace Elementor\Modules\Checklist;
use Elementor\Modules\Checklist\Steps\Create_Pages;
use Elementor\Modules\Checklist\Steps\Step_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Steps_Manager {
/** @var Step_Base[] $step_instances */
private array $step_instances = [];
private Checklist_Module_Interface $module;
public function __construct( Checklist_Module_Interface $module ) {
$this->module = $module;
$this->register_steps();
}
/**
* Gets formatted and ordered array of step ( step data, is_marked_completed and is_completed )
*
* @return array
*/
public function get_steps_for_frontend() : array {
$formatted_steps = [];
foreach ( $this->get_step_ids() as $step_id ) {
$instance = $this->step_instances[ $step_id ];
$is_marked_as_completed = $instance->is_marked_as_completed();
$step = [
'should_allow_undo' => $is_marked_as_completed,
'is_completed' => $instance->is_immutable_completed() || $instance->is_marked_as_completed() || $instance->is_absolute_completed(),
'config' => $this->get_step_config( $step_id ),
];
$formatted_steps[] = $step;
}
return $formatted_steps;
}
/**
* Marks a step as completed, returns true if the step was found and marked or false otherwise
*
* @param string $step_id
*
* @return void
*/
public function mark_step_as_completed( string $step_id ) : void {
foreach ( $this->step_instances as $step ) {
if ( $step->get_id() === $step_id ) {
$step->mark_as_completed();
return;
}
}
}
/**
* Unmarks a step as completed, returns true if the step was found and unmarked or false otherwise
*
* @param string $step_id
*
* @return void
*/
public function unmark_step_as_completed( string $step_id ) : void {
foreach ( $this->step_instances as $step ) {
if ( $step->get_id() === $step_id ) {
$step->unmark_as_completed();
return;
}
}
}
/**
* Maybe marks a step as completed (depending on if source allows it), returns true if the step was found and marked or false otherwise
*
* @param $step_id
*
* @return void
*/
public function maybe_set_step_as_immutable_completed( string $step_id ) : void {
foreach ( $this->step_instances as $step ) {
if ( $step->get_id() === $step_id ) {
$step->maybe_mark_as_completed();
return;
}
}
}
public function get_step_by_id( string $step_id ) : ?Step_Base {
return $this->step_instances[ $step_id ] ?? null;
}
/**
* @return array
*/
public function get_step_config( $step_id ) : array {
$step_instance = $this->step_instances[ $step_id ];
return $step_instance
? [
'id' => $step_instance->get_id(),
'title' => $step_instance->get_title(),
'description' => $step_instance->get_description(),
'learn_more_text' => $step_instance->get_learn_more_text(),
'learn_more_url' => $step_instance->get_learn_more_url(),
'cta_text' => $step_instance->get_cta_text(),
'cta_url' => $step_instance->get_cta_url(),
Step_Base::IS_COMPLETION_IMMUTABLE => $step_instance->get_is_completion_immutable(),
]
: [];
}
/**
* Getting the step instances array based on source's order
*
* @return void
*/
private function register_steps() : void {
foreach ( $this->get_step_ids() as $step_id ) {
$step_instance = $this->get_step_instance( $step_id );
if ( $step_instance && ! isset( $this->step_instances[ $step_id ] ) ) {
$this->step_instances[ $step_id ] = $step_instance;
}
}
}
/**
* Using step data->id, instanciates and returns the step class or null if the class does not exist
*
* @param $step_data
*
* @return Step_Base|null
*/
private function get_step_instance( string $step_id ) : ?Step_Base {
$class_name = '\\Elementor\\Modules\\Checklist\\Steps\\' . $step_id;
if ( ! class_exists( $class_name ) ) {
return null;
}
/** @var Step_Base $step */
return new $class_name( $this->module, $this->module->get_wordpress_adapter() );
}
/**
* Returns the steps config from source
*
* @return array
*/
private static function get_step_ids() : array {
return [ Create_Pages::STEP_ID ];
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Elementor\Modules\Checklist\Steps;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Create_Pages extends Step_Base {
const STEP_ID = 'create_pages';
public function get_id() : string {
return self::STEP_ID;
}
public function is_absolute_completed() : bool {
$pages = $this->wordpress_adapter->get_pages( [
'meta_key' => '_elementor_version',
'number' => 3,
] ) ?? [];
return count( $pages ) >= 3;
}
public function get_title() : string {
return esc_html__( 'Create your first 3 pages', 'elementor' );
}
public function get_description() : string {
return esc_html__( 'Jumpstart your creation with professional designs form the Template Library or start from scratch.', 'elementor' );
}
public function get_cta_text() : string {
return esc_html__( 'Create a new page', 'elementor' );
}
public function get_cta_url() : string {
return Plugin::$instance->documents->get_create_new_post_url( 'page' );
}
public function get_is_completion_immutable() : bool {
return true;
}
}

View File

@@ -0,0 +1,163 @@
<?php
namespace Elementor\Modules\Checklist\Steps;
use Elementor\Core\Isolation\Wordpress_Adapter;
use Elementor\Core\Isolation\Wordpress_Adapter_Interface;
use Elementor\Modules\Checklist\Module as Checklist_Module;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Step_Base {
/**
* @var string
* This is the key to be set to true if the step can be completed, and still be considered completed even if the user later did something to the should have it marked as not completed
*/
const IS_COMPLETION_IMMUTABLE = 'is_completion_immutable';
const MARKED_AS_COMPLETED_KEY = 'is_marked_completed';
const IMMUTABLE_COMPLETION_KEY = 'is_completed';
protected array $user_progress;
protected Wordpress_Adapter_Interface $wordpress_adapter;
protected Checklist_Module $module;
/**
* Returns a steps current completion status
*
* @return bool
*/
abstract protected function is_absolute_completed() : bool;
/**
* @return string
*/
abstract public function get_id() : string;
/**
* @return string
*/
abstract public function get_title() : string;
/**
* @return string
*/
abstract public function get_description() : string;
/**
* For instance; 'Create 3 pages'
* @return string
*/
abstract public function get_cta_text() : string;
/**
* @return string
*/
abstract public function get_cta_url() : string;
/**
* @return bool
*/
abstract public function get_is_completion_immutable() : bool;
/**
* Step_Base constructor.
*
* @param Checklist_Module $module
* @param ?Wordpress_Adapter_Interface $wordpress_adapter
* @return void
*/
public function __construct( Checklist_Module $module, ?Wordpress_Adapter_Interface $wordpress_adapter = null ) {
$this->module = $module;
$this->wordpress_adapter = $wordpress_adapter ?? new Wordpress_Adapter();
$this->user_progress = $module->get_step_progress( $this->get_id() ) ?? $this->get_step_initial_progress();
}
public function get_learn_more_text() : string {
return esc_html__( 'Learn more', 'elementor' );
}
public function get_learn_more_url() : string {
return 'https://go.elementor.com/getting-started-with-elementor/';
}
/**
* Marking a step as completed based on user's desire
*
* @return void
*/
public function mark_as_completed() : void {
$this->user_progress[ self::MARKED_AS_COMPLETED_KEY ] = true;
$this->set_step_progress();
}
/**
* Unmarking a step as completed based on user's desire
*
* @return void
*/
public function unmark_as_completed() : void {
$this->user_progress[ self::MARKED_AS_COMPLETED_KEY ] = false;
$this->set_step_progress();
}
/**
* Marking a step as completed if it was completed once, and it's suffice to marketing's requirements
*
* @return void
*/
public function maybe_mark_as_completed() : void {
$is_immutable_completed = $this->user_progress[ self::IMMUTABLE_COMPLETION_KEY ] ?? false;
if ( ! $is_immutable_completed && $this->get_is_completion_immutable() && $this->is_absolute_completed() ) {
$this->user_progress[ self::IMMUTABLE_COMPLETION_KEY ] = true;
$this->set_step_progress();
}
}
/**
* Returns the step marked as completed value
*
* @return bool
*/
public function is_marked_as_completed() : bool {
return $this->user_progress[ self::MARKED_AS_COMPLETED_KEY ];
}
/**
* Returns the step completed value
*
* @return bool
*/
public function is_immutable_completed() : bool {
return $this->user_progress[ self::IMMUTABLE_COMPLETION_KEY ];
}
/**
* Sets and returns the initial progress of the step
*
* @return array
*/
public function get_step_initial_progress() : array {
$initial_progress = [
self::MARKED_AS_COMPLETED_KEY => false,
self::IMMUTABLE_COMPLETION_KEY => false,
];
$this->module->set_step_progress( $this->get_id(), $initial_progress );
return $initial_progress;
}
/**
* Sets the step progress
*
* @return void
*/
private function set_step_progress() : void {
$this->module->set_step_progress( $this->get_id(), $this->user_progress );
}
}

View File

@@ -0,0 +1,156 @@
<?php
namespace Elementor\Modules\CompatibilityTag;
use Elementor\Plugin;
use Elementor\Core\Utils\Version;
use Elementor\Core\Utils\Collection;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Modules\System_Info\Module as System_Info;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Base_Module extends BaseModule {
const MODULE_NAME = 'compatibility-tag';
/**
* @var Compatibility_Tag
*/
private $compatibility_tag_service;
/**
* @return string
*/
public function get_name() {
return static::MODULE_NAME;
}
/**
* @return Compatibility_Tag
*/
private function get_compatibility_tag_service() {
if ( ! $this->compatibility_tag_service ) {
$this->compatibility_tag_service = new Compatibility_Tag( $this->get_plugin_header() );
}
return $this->compatibility_tag_service;
}
/**
* Add allowed headers to plugins.
*
* @param array $headers
* @param $compatibility_tag_header
*
* @return array
*/
protected function enable_elementor_headers( array $headers, $compatibility_tag_header ) {
$headers[] = $compatibility_tag_header;
return $headers;
}
/**
* @return Collection
*/
protected function get_plugins_to_check() {
return $this->get_plugins_with_header();
}
/**
* Append a compatibility message to the update plugin warning.
*
* @param array $args
*
* @throws \Exception
*/
protected function on_plugin_update_message( array $args ) {
$new_version = Version::create_from_string( $args['new_version'] );
if ( $new_version->compare( '=', $args['Version'], Version::PART_MAJOR_2 ) ) {
return;
}
$plugins = $this->get_plugins_to_check();
$plugins_compatibility = $this->get_compatibility_tag_service()->check( $new_version, $plugins->keys() );
$plugins = $plugins->filter( function ( $data, $plugin_name ) use ( $plugins_compatibility ) {
return Compatibility_Tag::COMPATIBLE !== $plugins_compatibility[ $plugin_name ];
} );
if ( $plugins->is_empty() ) {
return;
}
include __DIR__ . '/views/plugin-update-message-compatibility.php';
}
/**
* Get all plugins with specific header.
*
* @return Collection
*/
private function get_plugins_with_header() {
return Plugin::$instance->wp
->get_plugins()
->filter( function ( array $plugin ) {
return ! empty( $plugin[ $this->get_plugin_header() ] );
} );
}
/**
* @return string
*/
abstract protected function get_plugin_header();
/**
* @return string
*/
abstract protected function get_plugin_label();
/**
* @return string
*/
abstract protected function get_plugin_name();
/**
* @return string
*/
abstract protected function get_plugin_version();
/**
* Base_Module constructor.
*
* @throws \Exception
*/
public function __construct() {
add_filter( 'extra_plugin_headers', function ( array $headers ) {
return $this->enable_elementor_headers( $headers, $this->get_plugin_header() );
} );
add_action( 'in_plugin_update_message-' . $this->get_plugin_name(), function ( array $args ) {
$this->on_plugin_update_message( $args );
}, 11 /* After the warning message for backup */ );
add_action( 'elementor/system_info/get_allowed_reports', function () {
$plugin_short_name = basename( $this->get_plugin_name(), '.php' );
System_Info::add_report(
"{$plugin_short_name}_compatibility",
[
'file_name' => __DIR__ . '/compatibility-tag-report.php',
'class_name' => __NAMESPACE__ . '\Compatibility_Tag_Report',
'fields' => [
'compatibility_tag_service' => $this->get_compatibility_tag_service(),
'plugin_label' => $this->get_plugin_label(),
'plugin_version' => Version::create_from_string( $this->get_plugin_version() ),
'plugins_to_check' => $this->get_plugins_to_check()
->only( get_option( 'active_plugins' ) )
->keys(),
],
]
);
} );
}
}

View File

@@ -0,0 +1,176 @@
<?php
namespace Elementor\Modules\CompatibilityTag;
use Elementor\Plugin;
use Elementor\Core\Utils\Version;
use Elementor\Core\Utils\Collection;
use Elementor\Modules\System_Info\Reporters\Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Compatibility_Tag_Report extends Base {
/**
* @var Compatibility_Tag
*/
protected $compatibility_tag_service;
/**
* @var Version
*/
protected $plugin_version;
/**
* @var string
*/
protected $plugin_label;
/**
* @var array
*/
protected $plugins_to_check;
/**
* Compatibility_Tag_Report constructor.
*
* @param $properties
*/
public function __construct( $properties ) {
parent::__construct( $properties );
$this->compatibility_tag_service = $this->_properties['fields']['compatibility_tag_service'];
$this->plugin_label = $this->_properties['fields']['plugin_label'];
$this->plugin_version = $this->_properties['fields']['plugin_version'];
$this->plugins_to_check = $this->_properties['fields']['plugins_to_check'];
}
/**
* The title of the report
*
* @return string
*/
public function get_title() {
return $this->plugin_label . ' - Compatibility Tag';
}
/**
* Report fields
*
* @return string[]
*/
public function get_fields() {
return [
'report_data' => '',
];
}
/**
* Report data.
*
* @return string[]
* @throws \Exception
*/
public function get_report_data() {
$compatibility_status = $this->compatibility_tag_service->check(
$this->plugin_version,
$this->plugins_to_check
);
return [
'value' => $compatibility_status,
];
}
public function get_html_report_data() {
$compatibility_status = $this->compatibility_tag_service->check(
$this->plugin_version,
$this->plugins_to_check
);
$compatibility_status = $this->get_html_from_compatibility_status( $compatibility_status );
return [
'value' => $compatibility_status,
];
}
public function get_raw_report_data() {
$compatibility_status = $this->compatibility_tag_service->check(
$this->plugin_version,
$this->plugins_to_check
);
$compatibility_status = $this->get_raw_from_compatibility_status( $compatibility_status );
return [
'value' => $compatibility_status,
];
}
/**
* Merge compatibility status with the plugins data.
*
* @param array $compatibility_status
*
* @return Collection
*/
private function merge_compatibility_status_with_plugins( array $compatibility_status ) {
$labels = $this->get_report_labels();
$compatibility_status = ( new Collection( $compatibility_status ) )
->map( function ( $value ) use ( $labels ) {
$status = isset( $labels[ $value ] ) ? $labels[ $value ] : esc_html__( 'Unknown', 'elementor' );
return [ 'compatibility_status' => $status ];
} );
return Plugin::$instance->wp
->get_plugins()
->only( $compatibility_status->keys() )
->merge_recursive( $compatibility_status );
}
/**
* Format compatibility status into HTML.
*
* @param array $compatibility_status
*
* @return string
*/
private function get_html_from_compatibility_status( array $compatibility_status ) {
return $this->merge_compatibility_status_with_plugins( $compatibility_status )
->map( function ( array $plugin ) {
return "<tr><td> {$plugin['Name']} </td><td> {$plugin['compatibility_status']} </td></tr>";
} )
->implode( '' );
}
/**
* Format compatibility status into raw string.
*
* @param array $compatibility_status
*
* @return string
*/
private function get_raw_from_compatibility_status( array $compatibility_status ) {
return PHP_EOL . $this->merge_compatibility_status_with_plugins( $compatibility_status )
->map( function ( array $plugin ) {
return "\t{$plugin['Name']}: {$plugin['compatibility_status']}";
} )
->implode( PHP_EOL );
}
/**
* @return array
*/
private function get_report_labels() {
return [
Compatibility_Tag::COMPATIBLE => esc_html__( 'Compatible', 'elementor' ),
Compatibility_Tag::INCOMPATIBLE => esc_html__( 'Incompatible', 'elementor' ),
Compatibility_Tag::HEADER_NOT_EXISTS => esc_html__( 'Compatibility not specified', 'elementor' ),
Compatibility_Tag::INVALID_VERSION => esc_html__( 'Compatibility unknown', 'elementor' ),
Compatibility_Tag::PLUGIN_NOT_EXISTS => esc_html__( 'Error', 'elementor' ),
];
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace Elementor\Modules\CompatibilityTag;
use Elementor\Plugin;
use Elementor\Core\Utils\Version;
use Elementor\Core\Base\Base_Object;
use Elementor\Core\Utils\Collection;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Compatibility_Tag extends Base_Object {
const PLUGIN_NOT_EXISTS = 'plugin_not_exists';
const HEADER_NOT_EXISTS = 'header_not_exists';
const INVALID_VERSION = 'invalid_version';
const INCOMPATIBLE = 'incompatible';
const COMPATIBLE = 'compatible';
/**
* @var string Holds the header that should be checked.
*/
private $header;
/**
* Compatibility_Tag constructor.
*
* @param string $header
*/
public function __construct( $header ) {
$this->header = $header;
}
/**
* Return if plugins is compatible or not.
*
* @param Version $version
* @param array $plugins_names
*
* @return array
* @throws \Exception
*/
public function check( Version $version, array $plugins_names ) {
return ( new Collection( $plugins_names ) )
->map_with_keys( function ( $plugin_name ) use ( $version ) {
return [ $plugin_name => $this->is_compatible( $version, $plugin_name ) ];
} )
->all();
}
/**
* Check single plugin if is compatible or not.
*
* @param Version $version
* @param $plugin_name
*
* @return string
* @throws \Exception
*/
private function is_compatible( Version $version, $plugin_name ) {
$plugins = Plugin::$instance->wp->get_plugins();
if ( ! isset( $plugins[ $plugin_name ] ) ) {
return self::PLUGIN_NOT_EXISTS;
}
$requested_plugin = $plugins[ $plugin_name ];
if ( empty( $requested_plugin[ $this->header ] ) ) {
return self::HEADER_NOT_EXISTS;
}
if ( ! Version::is_valid_version( $requested_plugin[ $this->header ] ) ) {
return self::INVALID_VERSION;
}
if ( $version->compare( '>', $requested_plugin[ $this->header ], Version::PART_MAJOR_2 ) ) {
return self::INCOMPATIBLE;
}
return self::COMPATIBLE;
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Elementor\Modules\CompatibilityTag;
use Elementor\Plugin;
use Elementor\Core\Utils\Version;
use Elementor\Core\Utils\Collection;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Inspired By WooCommerce.
*
* @link https://github.com/woocommerce/woocommerce/blob/master/includes/admin/plugin-updates/class-wc-plugin-updates.php
*/
class Module extends Base_Module {
/**
* This is the header used by extensions to show testing.
*
* @var string
*/
const PLUGIN_VERSION_TESTED_HEADER = 'Elementor tested up to';
/**
* @return string
*/
protected function get_plugin_header() {
return static::PLUGIN_VERSION_TESTED_HEADER;
}
/**
* @return string
*/
protected function get_plugin_label() {
return esc_html__( 'Elementor', 'elementor' );
}
/**
* @return string
*/
protected function get_plugin_name() {
return ELEMENTOR_PLUGIN_BASE;
}
/**
* @return string
*/
protected function get_plugin_version() {
return ELEMENTOR_VERSION;
}
/**
* @return Collection
*/
protected function get_plugins_to_check() {
return parent::get_plugins_to_check()
->merge( $this->get_plugins_with_plugin_title_in_their_name() );
}
/**
* Get all the plugins that has the name of the current plugin in their name.
*
* @return Collection
*/
private function get_plugins_with_plugin_title_in_their_name() {
return Plugin::$instance->wp
->get_plugins()
->except( [
'elementor/elementor.php',
'elementor-beta/elementor-beta.php',
'block-builder/block-builder.php',
] )
->filter( function ( array $data ) {
return false !== strpos( strtolower( $data['Name'] ), 'elementor' );
} );
}
}

View File

@@ -0,0 +1,68 @@
<?php
use Elementor\Core\Utils\Version;
use Elementor\Core\Utils\Collection;
use Elementor\Modules\CompatibilityTag\Base_Module;
use Elementor\Modules\CompatibilityTag\Compatibility_Tag;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Those variables were declared in 'in_plugin_update_message' method that included the current view file.
*
* @var Base_Module $this
* @var Version $new_version
* @var Collection $plugins
* @var array $plugins_compatibility
*/
?>
<hr class="e-major-update-warning__separator" />
<div class="e-major-update-warning">
<div class="e-major-update-warning__icon">
<i class="eicon-info-circle"></i>
</div>
<div>
<div class="e-major-update-warning__message">
<strong>
<?php echo esc_html__( 'Compatibility Alert', 'elementor' ); ?>
</strong> -
<?php
echo sprintf(
/* translators: 1: Plugin name, 2: Plugin version. */
esc_html__( 'Some of the plugins youre using have not been tested with the latest version of %1$s (%2$s). To avoid issues, make sure they are all up to date and compatible before updating %1$s.', 'elementor' ),
esc_html( $this->get_plugin_label() ),
$new_version->__toString() // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
);
?>
</div>
<br />
<table class="e-compatibility-update-table">
<tr>
<th><?php echo esc_html__( 'Plugin', 'elementor' ); ?></th>
<th><?php
/* translators: %s: Elementor plugin name. */
echo sprintf( esc_html__( 'Tested up to %s version', 'elementor' ), esc_html( $this->get_plugin_label() ) );
?></th>
</tr>
<?php foreach ( $plugins as $plugin_name => $plugin_data ) : ?>
<?php
if (
in_array( $plugins_compatibility[ $plugin_name ], [
Compatibility_Tag::PLUGIN_NOT_EXISTS,
Compatibility_Tag::HEADER_NOT_EXISTS,
Compatibility_Tag::INVALID_VERSION,
], true )
) {
$plugin_data[ $this->get_plugin_header() ] = esc_html__( 'Unknown', 'elementor' );
}
?>
<tr>
<td><?php echo esc_html( $plugin_data['Name'] ); ?></td>
<td><?php echo esc_html( $plugin_data[ $this->get_plugin_header() ] ); ?></td>
</tr>
<?php endforeach ?>
</table>
</div>
</div>

View File

@@ -0,0 +1,170 @@
<?php
namespace Elementor\Modules\ContainerConverter;
use Elementor\Controls_Manager;
use Elementor\Controls_Stack;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Module extends \Elementor\Core\Base\Module {
// Event name dispatched by the buttons.
const EVENT_NAME = 'elementorContainerConverter:convert';
/**
* Retrieve the module name.
*
* @return string
*/
public function get_name() {
return 'container-converter';
}
/**
* Determine whether the module is active.
*
* @return bool
*/
public static function is_active() {
return Plugin::$instance->experiments->is_feature_active( 'container' );
}
/**
* Enqueue the module scripts.
*
* @return void
*/
public function enqueue_scripts() {
wp_enqueue_script(
'container-converter',
$this->get_js_assets_url( 'container-converter' ),
[ 'elementor-editor' ],
ELEMENTOR_VERSION,
true
);
}
/**
* Enqueue the module styles.
*
* @return void
*/
public function enqueue_styles() {
wp_enqueue_style(
'container-converter',
$this->get_css_assets_url( 'modules/container-converter/editor' ),
[],
ELEMENTOR_VERSION
);
}
/**
* Add a convert button to sections.
*
* @param \Elementor\Controls_Stack $controls_stack
*
* @return void
*/
protected function add_section_convert_button( Controls_Stack $controls_stack ) {
if ( ! Plugin::$instance->editor->is_edit_mode() ) {
return;
}
$controls_stack->start_injection( [
'of' => '_title',
] );
$controls_stack->add_control(
'convert_to_container',
[
'type' => Controls_Manager::BUTTON,
'label' => esc_html__( 'Convert to container', 'elementor' ),
'text' => esc_html__( 'Convert', 'elementor' ),
'button_type' => 'default',
'description' => esc_html__( 'Copies all of the selected sections and columns and pastes them in a container beneath the original.', 'elementor' ),
'separator' => 'after',
'event' => static::EVENT_NAME,
]
);
$controls_stack->end_injection();
}
/**
* Add a convert button to page settings.
*
* @param \Elementor\Controls_Stack $controls_stack
*
* @return void
*/
protected function add_page_convert_button( Controls_Stack $controls_stack ) {
if ( ! Plugin::$instance->editor->is_edit_mode() || ! $this->page_contains_sections( $controls_stack ) || ! Plugin::$instance->role_manager->user_can( 'design' ) ) {
return;
}
$controls_stack->start_injection( [
'of' => 'post_title',
'at' => 'before',
] );
$controls_stack->add_control(
'convert_to_container',
[
'type' => Controls_Manager::BUTTON,
'label' => esc_html__( 'Convert to container', 'elementor' ),
'text' => esc_html__( 'Convert', 'elementor' ),
'button_type' => 'default',
'description' => esc_html__( 'Copies all of the selected sections and columns and pastes them in a container beneath the original.', 'elementor' ),
'separator' => 'after',
'event' => static::EVENT_NAME,
]
);
$controls_stack->end_injection();
}
/**
* Checks if document has any Section elements.
*
* @param \Elementor\Controls_Stack $controls_stack
*
* @return bool
*/
protected function page_contains_sections( $controls_stack ) {
$data = $controls_stack->get_elements_data();
if ( ! is_array( $data ) ) {
return false;
}
foreach ( $data as $element ) {
if ( isset( $element['elType'] ) && 'section' === $element['elType'] ) {
return true;
}
}
return false;
}
/**
* Initialize the Container-Converter module.
*
* @return void
*/
public function __construct() {
add_action( 'elementor/editor/after_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
add_action( 'elementor/editor/after_enqueue_styles', [ $this, 'enqueue_styles' ] );
add_action( 'elementor/element/section/section_layout/after_section_end', function ( Controls_Stack $controls_stack ) {
$this->add_section_convert_button( $controls_stack );
} );
add_action( 'elementor/documents/register_controls', function ( Controls_Stack $controls_stack ) {
$this->add_page_convert_button( $controls_stack );
} );
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Elementor\Modules\ContentSanitizer\Interfaces;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
interface Sanitizable {
public function sanitize( $content );
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Elementor\Modules\ContentSanitizer;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseModule {
const WIDGET_TO_SANITIZE = 'heading';
public function __construct() {
parent::__construct();
add_filter( 'elementor/document/save/data', [ $this, 'sanitize_content' ], 10, 2 );
}
public function get_name() {
return 'content-sanitizer';
}
public function sanitize_content( $data, $document ) : array {
if ( current_user_can( 'manage_options' ) || empty( $data['elements'] ) ) {
return $data;
}
if ( ! $this->is_widget_present( $data ) ) {
return $data;
}
return Plugin::$instance->db->iterate_data( $data, function ( $element ) {
if ( $this->is_target_widget( $element ) ) {
$element['settings']['title'] = Plugin::$instance->widgets_manager->get_widget_types( self::WIDGET_TO_SANITIZE )->sanitize( $element['settings']['title'] );
}
return $element;
});
}
private function is_target_widget( $element ) {
return self::WIDGET_TO_SANITIZE === $element['widgetType'];
}
private function is_widget_present( array $elements ): bool {
$json = wp_json_encode( $elements );
return false !== strpos( $json, '"widgetType":"' . self::WIDGET_TO_SANITIZE . '"' );
}
}

View File

@@ -0,0 +1,369 @@
<?php
namespace Elementor\Modules\DevTools;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Deprecation {
const SOFT_VERSIONS_COUNT = 4;
const HARD_VERSIONS_COUNT = 8;
private $current_version = null;
private $soft_deprecated_notices = [];
public function __construct( $current_version ) {
$this->current_version = $current_version;
}
public function get_settings() {
return [
'soft_notices' => $this->soft_deprecated_notices,
'soft_version_count' => self::SOFT_VERSIONS_COUNT,
'hard_version_count' => self::HARD_VERSIONS_COUNT,
'current_version' => ELEMENTOR_VERSION,
];
}
/**
* Get total of major.
*
* Since `get_total_major` cannot determine how much really versions between 2.9.0 and 3.3.0 if there is 2.10.0 version for example,
* versions with major2 more then 9 will be added to total.
*
* @since 3.1.0
*
* @param array $parsed_version
*
* @return int
*/
public function get_total_major( $parsed_version ) {
$major1 = $parsed_version['major1'];
$major2 = $parsed_version['major2'];
$major2 = $major2 > 9 ? 9 : $major2;
$minor = 0;
$total = intval( "{$major1}{$major2}{$minor}" );
if ( $total > 99 ) {
$total = $total / 10;
} else {
$total = intval( $total / 10 );
}
if ( $parsed_version['major2'] > 9 ) {
$total += $parsed_version['major2'] - 9;
}
return $total;
}
/**
* Get next version.
*
* @since 3.1.0
*
* @param string $version
* @param int $count
*
* @return string|false
*/
public function get_next_version( $version, $count = 1 ) {
$version = $this->parse_version( $version );
if ( ! $version ) {
return false;
}
$version['total'] = $this->get_total_major( $version ) + $count;
$total = $version['total'];
if ( $total > 9 ) {
$version['major1'] = intval( $total / 10 );
$version['major2'] = $total % 10;
} else {
$version['major1'] = 0;
$version['major2'] = $total;
}
$version['minor'] = 0;
return $this->implode_version( $version );
}
/**
* Implode parsed version to string version.
*
* @since 3.1.0
*
* @param array $parsed_version
*
* @return string
*/
public function implode_version( $parsed_version ) {
$major1 = $parsed_version['major1'];
$major2 = $parsed_version['major2'];
$minor = $parsed_version['minor'];
return "{$major1}.{$major2}.{$minor}";
}
/**
* Parse to an informative array.
*
* @since 3.1.0
*
* @param string $version
*
* @return array|false
*/
public function parse_version( $version ) {
$version_explode = explode( '.', $version );
$version_explode_count = count( $version_explode );
if ( $version_explode_count < 3 || $version_explode_count > 4 ) {
trigger_error( 'Invalid Semantic Version string provided' );
return false;
}
list( $major1, $major2, $minor ) = $version_explode;
$result = [
'major1' => intval( $major1 ),
'major2' => intval( $major2 ),
'minor' => intval( $minor ),
];
if ( $version_explode_count > 3 ) {
$result['build'] = $version_explode[3];
}
return $result;
}
/**
* Compare two versions, result is equal to diff of major versions.
* Notice: If you want to compare between 2.9.0 and 3.3.0, and there is also a 2.10.0 version, you cannot get the right comparison
* Since $this->deprecation->get_total_major cannot determine how much really versions between 2.9.0 and 3.3.0.
*
* @since 3.1.0
*
* @param {string} $version1
* @param {string} $version2
*
* @return int|false
*/
public function compare_version( $version1, $version2 ) {
$version1 = self::parse_version( $version1 );
$version2 = self::parse_version( $version2 );
if ( $version1 && $version2 ) {
$versions = [ &$version1, &$version2 ];
foreach ( $versions as &$version ) {
$version['total'] = self::get_total_major( $version );
}
return $version1['total'] - $version2['total'];
}
return false;
}
/**
* Check Deprecation
*
* Checks whether the given entity is valid. If valid, this method checks whether the deprecation
* should be soft (browser console notice) or hard (use WordPress' native deprecation methods).
*
* @since 3.1.0
*
* @param string $entity - The Deprecated entity (the function/hook itself)
* @param string $version
* @param string $replacement Optional
* @param string $base_version Optional. Default is `null`
*
* @return bool|void
* @throws \Exception
*/
private function check_deprecation( $entity, $version, $replacement, $base_version = null ) {
if ( null === $base_version ) {
$base_version = $this->current_version;
}
$diff = $this->compare_version( $base_version, $version );
if ( false === $diff ) {
throw new \Exception( 'Invalid deprecation diff.' );
}
$print_deprecated = false;
if ( defined( 'WP_DEBUG' ) && WP_DEBUG && $diff <= self::SOFT_VERSIONS_COUNT ) {
// Soft deprecated.
if ( ! isset( $this->soft_deprecated_notices[ $entity ] ) ) {
$this->soft_deprecated_notices[ $entity ] = [
$version,
$replacement,
];
}
if ( defined( 'ELEMENTOR_DEBUG' ) && ELEMENTOR_DEBUG ) {
$print_deprecated = true;
}
}
return $print_deprecated;
}
/**
* Deprecated Function
*
* Handles the deprecation process for functions.
*
* @since 3.1.0
*
* @param string $function
* @param string $version
* @param string $replacement Optional. Default is ''
* @param string $base_version Optional. Default is `null`
* @throws \Exception
*/
public function deprecated_function( $function, $version, $replacement = '', $base_version = null ) {
$print_deprecated = $this->check_deprecation( $function, $version, $replacement, $base_version );
if ( $print_deprecated ) {
// PHPCS - We need to echo special characters because they can exist in function calls.
_deprecated_function( $function, esc_html( $version ), $replacement ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
}
/**
* Deprecated Hook
*
* Handles the deprecation process for hooks.
*
* @since 3.1.0
*
* @param string $hook
* @param string $version
* @param string $replacement Optional. Default is ''
* @param string $base_version Optional. Default is `null`
* @throws \Exception
*/
public function deprecated_hook( $hook, $version, $replacement = '', $base_version = null ) {
$print_deprecated = $this->check_deprecation( $hook, $version, $replacement, $base_version );
if ( $print_deprecated ) {
_deprecated_hook( esc_html( $hook ), esc_html( $version ), esc_html( $replacement ) );
}
}
/**
* Deprecated Argument
*
* Handles the deprecation process for function arguments.
*
* @since 3.1.0
*
* @param string $argument
* @param string $version
* @param string $replacement
* @param string $message
* @throws \Exception
*/
public function deprecated_argument( $argument, $version, $replacement = '', $message = '' ) {
$print_deprecated = $this->check_deprecation( $argument, $version, $replacement );
if ( $print_deprecated ) {
$message = empty( $message ) ? '' : ' ' . $message;
// These arguments are escaped because they are printed later, and are not escaped when printed.
$error_message_args = [ esc_html( $argument ), esc_html( $version ) ];
if ( $replacement ) {
/* translators: 1: Function argument, 2: Elementor version number, 3: Replacement argument name. */
$translation_string = esc_html__( 'The %1$s argument is deprecated since version %2$s! Use %3$s instead.', 'elementor' );
$error_message_args[] = $replacement;
} else {
/* translators: 1: Function argument, 2: Elementor version number. */
$translation_string = esc_html__( 'The %1$s argument is deprecated since version %2$s!', 'elementor' );
}
trigger_error(
vsprintf(
// PHPCS - $translation_string is already escaped above.
$translation_string, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
// PHPCS - $error_message_args is an array.
$error_message_args // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
) . esc_html( $message ),
E_USER_DEPRECATED
);
}
}
/**
* Do Deprecated Action
*
* A method used to run deprecated actions through Elementor's deprecation process.
*
* @since 3.1.0
*
* @param string $hook
* @param array $args
* @param string $version
* @param string $replacement
* @param null|string $base_version
*
* @throws \Exception
*/
public function do_deprecated_action( $hook, $args, $version, $replacement = '', $base_version = null ) {
if ( ! has_action( $hook ) ) {
return;
}
$this->deprecated_hook( $hook, $version, $replacement, $base_version );
do_action_ref_array( $hook, $args );
}
/**
* Apply Deprecated Filter
*
* A method used to run deprecated filters through Elementor's deprecation process.
*
* @since 3.2.0
*
* @param string $hook
* @param array $args
* @param string $version
* @param string $replacement
* @param null|string $base_version
*
* @return mixed
* @throws \Exception
*/
public function apply_deprecated_filter( $hook, $args, $version, $replacement = '', $base_version = null ) {
if ( ! has_action( $hook ) ) {
// `$args` should be an array, but in order to keep BC, we need to support non-array values.
if ( is_array( $args ) ) {
return $args[0] ?? null;
}
return $args;
}
// BC - See the comment above.
if ( ! is_array( $args ) ) {
$args = [ $args ];
}
// Avoid associative arrays.
$args = array_values( $args );
$this->deprecated_hook( $hook, $version, $replacement, $base_version );
return apply_filters_ref_array( $hook, $args );
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Elementor\Modules\DevTools;
use Elementor\Core\Base\App;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Fix issue with 'Potentially polymorphic call. The code may be inoperable depending on the actual class instance passed as the argument.'.
* Its tells to the editor that instance() return right module. instead of base module.
* @method Module instance()
*/
class Module extends App {
/**
* @var Deprecation
*/
public $deprecation;
public function __construct() {
$this->deprecation = new Deprecation( ELEMENTOR_VERSION );
add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'register_scripts' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'register_scripts' ] );
add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts' ] );
add_action( 'elementor/frontend/after_register_scripts', [ $this, 'register_scripts' ] );
add_action( 'elementor/common/after_register_scripts', [ $this, 'register_scripts' ] );
}
public function get_name() {
return 'dev-tools';
}
public function register_scripts() {
wp_register_script(
'elementor-dev-tools',
$this->get_js_assets_url( 'dev-tools' ),
[],
ELEMENTOR_VERSION,
true
);
$this->print_config( 'elementor-dev-tools' );
}
protected function get_init_settings() {
return [
'isDebug' => ( defined( 'WP_DEBUG' ) && WP_DEBUG ),
'urls' => [
'assets' => ELEMENTOR_ASSETS_URL,
],
'deprecation' => $this->deprecation->get_settings(),
];
}
}

View File

@@ -0,0 +1,165 @@
<?php
namespace Elementor\Modules\DynamicTags;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\DynamicTags\Base_Tag;
use Elementor\Core\DynamicTags\Manager;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor dynamic tags module.
*
* Elementor dynamic tags module handler class is responsible for registering
* and managing Elementor dynamic tags modules.
*
* @since 2.0.0
*/
class Module extends BaseModule {
/**
* Base dynamic tag group.
*/
const BASE_GROUP = 'base';
/**
* Dynamic tags text category.
*/
const TEXT_CATEGORY = 'text';
/**
* Dynamic tags URL category.
*/
const URL_CATEGORY = 'url';
/**
* Dynamic tags image category.
*/
const IMAGE_CATEGORY = 'image';
/**
* Dynamic tags media category.
*/
const MEDIA_CATEGORY = 'media';
/**
* Dynamic tags post meta category.
*/
const POST_META_CATEGORY = 'post_meta';
/**
* Dynamic tags gallery category.
*/
const GALLERY_CATEGORY = 'gallery';
/**
* Dynamic tags number category.
*/
const NUMBER_CATEGORY = 'number';
/**
* Dynamic tags number category.
*/
const COLOR_CATEGORY = 'color';
/**
* Dynamic tags datetime category.
*/
const DATETIME_CATEGORY = 'datetime';
/**
* Dynamic tags module constructor.
*
* Initializing Elementor dynamic tags module.
*
* @since 2.0.0
* @access public
*/
public function __construct() {
$this->register_groups();
add_action( 'elementor/dynamic_tags/register', [ $this, 'register_tags' ] );
}
/**
* Get module name.
*
* Retrieve the dynamic tags module name.
*
* @since 2.0.0
* @access public
*
* @return string Module name.
*/
public function get_name() {
return 'dynamic_tags';
}
/**
* Get classes names.
*
* Retrieve the dynamic tag classes names.
*
* @since 2.0.0
* @access public
*
* @return array Tag dynamic tag classes names.
*/
public function get_tag_classes_names() {
return [];
}
/**
* Get groups.
*
* Retrieve the dynamic tag groups.
*
* @since 2.0.0
* @access public
*
* @return array Tag dynamic tag groups.
*/
public function get_groups() {
return [
self::BASE_GROUP => [
'title' => 'Base Tags',
],
];
}
/**
* Register groups.
*
* Add all the available tag groups.
*
* @since 2.0.0
* @access private
*/
private function register_groups() {
foreach ( $this->get_groups() as $group_name => $group_settings ) {
Plugin::$instance->dynamic_tags->register_group( $group_name, $group_settings );
}
}
/**
* Register tags.
*
* Add all the available dynamic tags.
*
* @since 2.0.0
* @access public
*
* @param Manager $dynamic_tags
*/
public function register_tags( $dynamic_tags ) {
foreach ( $this->get_tag_classes_names() as $tag_class ) {
/** @var Base_Tag $class_name */
$class_name = $this->get_reflection()->getNamespaceName() . '\Tags\\' . $tag_class;
$dynamic_tags->register( new $class_name() );
}
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Elementor\Modules\EditorAppBar;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Experiments\Manager as Experiments_Manager;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseModule {
const EXPERIMENT_NAME = 'editor_v2'; // Kept as `editor_v2` for backward compatibility.
const PACKAGES = [
'editor-app-bar',
'editor-documents',
'editor-panels',
'editor-site-navigation',
];
const STYLES = [
'editor-v2-app-bar-overrides',
];
public function get_name() {
return 'editor-app-bar';
}
public function __construct() {
parent::__construct();
$this->register_experiment();
if ( Plugin::$instance->experiments->is_feature_active( self::EXPERIMENT_NAME ) ) {
add_filter( 'elementor/editor/v2/packages', fn( $packages ) => $this->add_packages( $packages ) );
add_filter( 'elementor/editor/v2/styles', fn( $styles ) => $this->add_styles( $styles ) );
add_filter( 'elementor/editor/templates', fn( $templates ) => $this->remove_templates( $templates ) );
add_action( 'elementor/editor/v2/scripts/enqueue', fn() => $this->dequeue_scripts() );
add_action( 'elementor/editor/v2/styles/enqueue', fn() => $this->dequeue_styles() );
}
}
private function register_experiment() {
Plugin::$instance->experiments->add_feature( [
'name' => self::EXPERIMENT_NAME,
'title' => esc_html__( 'Editor Top Bar', 'elementor' ),
'description' => sprintf(
'%1$s <a href="https://go.elementor.com/wp-dash-elementor-top-bar/" target="_blank">%2$s</a>',
esc_html__( 'Get a sneak peek of the new Editor powered by React. The beautiful design and experimental layout of the Top bar are just some of the exciting tools on their way.', 'elementor' ),
esc_html__( 'Learn more', 'elementor' )
),
'default' => Experiments_Manager::STATE_INACTIVE,
'release_status' => Experiments_Manager::RELEASE_STATUS_STABLE,
'new_site' => [
'default_active' => true,
'minimum_installation_version' => '3.23.0',
],
] );
}
private function add_packages( $packages ) {
return array_merge( $packages, self::PACKAGES );
}
private function add_styles( $styles ) {
return array_merge( $styles, self::STYLES );
}
private function remove_templates( $templates ) {
return array_diff( $templates, [ 'responsive-bar' ] );
}
private function dequeue_scripts() {
wp_dequeue_script( 'elementor-responsive-bar' );
}
private function dequeue_styles() {
wp_dequeue_style( 'elementor-responsive-bar' );
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Elementor\Modules\EditorEvents;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Common\Modules\Connect\Apps\Base_App;
use Elementor\Core\Experiments\Manager as Experiments_Manager;
use Elementor\Utils;
use Elementor\Plugin;
use Elementor\Tracker;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseModule {
const EXPERIMENT_NAME = 'editor_events';
public function __construct() {
parent::__construct();
$this->register_experiment();
}
public function get_name() {
return 'editor-events';
}
public static function get_editor_events_config() {
$can_send_events = defined( 'ELEMENTOR_EDITOR_EVENTS_MIXPANEL_TOKEN' ) &&
Tracker::is_allow_track() &&
Plugin::$instance->experiments->is_feature_active( self::EXPERIMENT_NAME );
$settings = [
'can_send_events' => $can_send_events,
'elementor_version' => ELEMENTOR_VERSION,
'site_url' => hash( 'sha256', get_site_url() ),
'wp_version' => get_bloginfo( 'version' ),
'user_agent' => esc_html( Utils::get_super_global_value( $_SERVER, 'HTTP_USER_AGENT' ) ),
'site_language' => get_locale(),
'site_key' => get_option( Base_App::OPTION_CONNECT_SITE_KEY ),
'subscription_id' => null,
'token' => defined( 'ELEMENTOR_EDITOR_EVENTS_MIXPANEL_TOKEN' ) ? ELEMENTOR_EDITOR_EVENTS_MIXPANEL_TOKEN : '',
];
return $settings;
}
private function register_experiment() {
Plugin::$instance->experiments->add_feature( [
'name' => static::EXPERIMENT_NAME,
'title' => esc_html__( 'Elementor Editor Events', 'elementor' ),
'description' => esc_html__( 'Editor events processing', 'elementor' ),
'hidden' => true,
'default' => Experiments_Manager::STATE_INACTIVE,
] );
}
}

View File

@@ -0,0 +1,159 @@
<?php
namespace Elementor\Modules\ElementCache;
use Elementor\Controls_Manager;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Experiments\Manager as ExperimentsManager;
use Elementor\Element_Base;
use Elementor\Plugin;
use Elementor\Settings;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseModule {
public function get_name() {
return 'element-cache';
}
public function __construct() {
parent::__construct();
$this->register_experiments();
$this->register_shortcode();
if ( ! Plugin::$instance->experiments->is_feature_active( 'e_element_cache' ) ) {
return;
}
$this->add_advanced_tab_actions();
if ( is_admin() ) {
add_action( 'elementor/admin/after_create_settings/' . Settings::PAGE_ID, [ $this, 'register_admin_fields' ], 100 );
}
$this->clear_cache_on_site_changed();
}
private function register_experiments() {
Plugin::$instance->experiments->add_feature( [
'name' => 'e_element_cache',
'title' => esc_html__( 'Element Caching', 'elementor' ),
'tag' => esc_html__( 'Performance', 'elementor' ),
'description' => esc_html__( 'Elements caching reduces loading times by serving up a copy of an element instead of rendering it fresh every time the page is loaded. When active, Elementor will determine which elements can benefit from static loading - but you can override this.', 'elementor' ),
'release_status' => ExperimentsManager::RELEASE_STATUS_BETA,
'default' => ExperimentsManager::STATE_INACTIVE,
'new_site' => [
'default_active' => true,
'minimum_installation_version' => '3.23.0',
],
'generator_tag' => true,
] );
}
private function register_shortcode() {
add_shortcode( 'elementor-element', function ( $atts ) {
if ( empty( $atts['data'] ) ) {
return '';
}
$widget_data = json_decode( base64_decode( $atts['data'] ), true );
if ( empty( $widget_data ) || ! is_array( $widget_data ) ) {
return '';
}
ob_start();
$element = Plugin::$instance->elements_manager->create_element_instance( $widget_data );
if ( $element ) {
$element->print_element();
}
return ob_get_clean();
} );
}
private function add_advanced_tab_actions() {
$hooks = array(
'elementor/element/common/_section_style/after_section_end' => '_css_classes', // Widgets
);
foreach ( $hooks as $hook => $injection_position ) {
add_action(
$hook,
function( $element, $args ) use ( $injection_position ) {
$this->add_control_to_advanced_tab( $element, $args, $injection_position );
},
10,
2
);
}
}
private function add_control_to_advanced_tab( Element_Base $element, $args, $injection_position ) {
$element->start_injection(
[
'of' => $injection_position,
]
);
$control_data = [
'label' => esc_html__( 'Cache Settings', 'elementor' ),
'type' => Controls_Manager::SELECT,
'default' => '',
'options' => [
'' => esc_html__( 'Default', 'elementor' ),
'yes' => esc_html__( 'Inactive', 'elementor' ),
'no' => esc_html__( 'Active', 'elementor' ),
],
];
$element->add_control( '_element_cache', $control_data );
$element->end_injection();
}
public function register_admin_fields( Settings $settings ) {
$settings->add_field(
Settings::TAB_PERFORMANCE,
Settings::TAB_PERFORMANCE,
'element_cache_ttl',
[
'label' => esc_html__( 'Element Cache Expiration', 'elementor' ),
'field_args' => [
'class' => 'elementor-element-cache-ttl',
'type' => 'select',
'std' => '24',
'options' => [
'1' => esc_html__( '1 Hour', 'elementor' ),
'6' => esc_html__( '6 Hours', 'elementor' ),
'12' => esc_html__( '12 Hours', 'elementor' ),
'24' => esc_html__( '1 Day', 'elementor' ),
'72' => esc_html__( '3 Days', 'elementor' ),
'168' => esc_html__( '1 Week', 'elementor' ),
'336' => esc_html__( '2 Weeks', 'elementor' ),
'720' => esc_html__( '1 Month', 'elementor' ),
'8760' => esc_html__( '1 Year', 'elementor' ),
],
'desc' => esc_html__( 'Specify the duration for which data is stored in the cache. Elements caching speeds up loading by serving pre-rendered copies of elements, rather than rendering them fresh each time. This control ensures efficient performance and up-to-date content.', 'elementor' ),
],
]
);
}
private function clear_cache_on_site_changed() {
add_action( 'activated_plugin', [ $this, 'clear_cache' ] );
add_action( 'deactivated_plugin', [ $this, 'clear_cache' ] );
add_action( 'switch_theme', [ $this, 'clear_cache' ] );
add_action( 'upgrader_process_complete', [ $this, 'clear_cache' ] );
}
public function clear_cache() {
Plugin::$instance->files_manager->clear_cache();
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Elementor\Modules\ElementManager;
use Elementor\Core\Admin\Menu\Interfaces\Admin_Menu_Item_With_Page;
use Elementor\Settings;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Admin_Menu_App implements Admin_Menu_Item_With_Page {
public function is_visible() {
return true;
}
public function get_parent_slug() {
return Settings::PAGE_ID;
}
public function get_label() {
return esc_html__( 'Element Manager', 'elementor' );
}
public function get_page_title() {
return esc_html__( 'Element Manager', 'elementor' );
}
public function get_capability() {
return 'manage_options';
}
public function render() {
echo '<div class="wrap">';
echo '<h3 class="wp-heading-inline">' . esc_html__( 'Element Manager', 'elementor' ) . '</h3>';
echo '<div id="elementor-element-manager-wrap"></div>';
echo '</div>';
}
}

View File

@@ -0,0 +1,183 @@
<?php
namespace Elementor\Modules\ElementManager;
use Elementor\Core\Utils\Promotions\Filtered_Promotions_Manager;
use Elementor\Modules\Usage\Module as Usage_Module;
use Elementor\Api;
use Elementor\Plugin;
use Elementor\User;
use Elementor\Utils;
use Elementor\Core\Utils\Promotions\Validate_Promotion;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Ajax {
const ELEMENT_MANAGER_PROMOTION_URL = 'https://go.elementor.com/go-pro-element-manager/';
const FREE_TO_PRO_PERMISSIONS_PROMOTION_URL = 'https://go.elementor.com/go-pro-element-manager-permissions/';
const PRO_TO_ADVANCED_PERMISSIONS_PROMOTION_URL = 'https://go.elementor.com/go-pro-advanced-element-manager-permissions/';
public function register_endpoints() {
add_action( 'wp_ajax_elementor_element_manager_get_admin_app_data', [ $this, 'ajax_get_admin_page_data' ] );
add_action( 'wp_ajax_elementor_element_manager_save_disabled_elements', [ $this, 'ajax_save_disabled_elements' ] );
add_action( 'wp_ajax_elementor_element_manager_get_widgets_usage', [ $this, 'ajax_get_widgets_usage' ] );
}
public function ajax_get_admin_page_data() {
$this->verify_permission();
$this->force_enabled_all_elements();
$widgets = [];
$plugins = [];
foreach ( Plugin::$instance->widgets_manager->get_widget_types() as $widget ) {
$widget_title = sanitize_user( $widget->get_title() );
if ( empty( $widget_title ) || ! $widget->show_in_panel() ) {
continue;
}
$plugin_name = $this->get_plugin_name_from_widget_instance( $widget );
if ( ! in_array( $plugin_name, $plugins ) ) {
$plugins[] = $plugin_name;
}
$widgets[] = [
'name' => $widget->get_name(),
'plugin' => $plugin_name,
'title' => $widget_title,
'icon' => $widget->get_icon(),
];
}
$notice_id = 'e-element-manager-intro-1';
$data = [
'disabled_elements' => Options::get_disabled_elements(),
'promotion_widgets' => [],
'widgets' => $widgets,
'plugins' => $plugins,
'notice_data' => [
'notice_id' => $notice_id,
'is_viewed' => User::is_user_notice_viewed( $notice_id ),
],
'promotion_data' => [
'manager_permissions' => [
'pro' => $this->get_element_manager_promotion(
[
'text' => esc_html__( 'Upgrade Now', 'elementor' ),
'url' => self::FREE_TO_PRO_PERMISSIONS_PROMOTION_URL,
],
'pro_permissions'
),
'advanced' => $this->get_element_manager_promotion(
[
'text' => esc_html__( 'Upgrade Now', 'elementor' ),
'url' => self::PRO_TO_ADVANCED_PERMISSIONS_PROMOTION_URL,
],
'advanced_permissions'
),
],
'element_manager' => $this->get_element_manager_promotion(
[
'text' => esc_html__( 'Upgrade Now', 'elementor' ),
'url' => self::ELEMENT_MANAGER_PROMOTION_URL,
],
'element_manager'
),
],
];
if ( ! Utils::has_pro() ) {
$data['promotion_widgets'] = Api::get_promotion_widgets();
}
$data['additional_data'] = apply_filters( 'elementor/element_manager/admin_app_data/additional_data', [] );
wp_send_json_success( $data );
}
private function get_element_manager_promotion( $promotion_data, $filter_id ): array {
return Filtered_Promotions_Manager::get_filtered_promotion_data( $promotion_data, 'elementor/element_manager/admin_app_data/promotion_data/' . $filter_id, 'url' );
}
private function verify_permission() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( esc_html__( 'You do not have permission to edit these settings.', 'elementor' ) );
}
$nonce = Utils::get_super_global_value( $_POST, 'nonce' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, 'e-element-manager-app' ) ) {
wp_send_json_error( esc_html__( 'Invalid nonce.', 'elementor' ) );
}
}
private function force_enabled_all_elements() {
remove_all_filters( 'elementor/widgets/is_widget_enabled' );
}
private function get_plugin_name_from_widget_instance( $widget ) {
if ( in_array( 'wordpress', $widget->get_categories() ) ) {
return esc_html__( 'WordPress Widgets', 'elementor' );
}
$class_reflection = new \ReflectionClass( $widget );
$plugin_basename = plugin_basename( $class_reflection->getFileName() );
$plugin_directory = strtok( $plugin_basename, '/' );
$plugins_data = get_plugins( '/' . $plugin_directory );
$plugin_data = array_shift( $plugins_data );
return $plugin_data['Name'] ?? esc_html__( 'Unknown', 'elementor' );
}
public function ajax_save_disabled_elements() {
$this->verify_permission();
$elements = Utils::get_super_global_value( $_POST, 'widgets' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
if ( empty( $elements ) ) {
wp_send_json_error( esc_html__( 'No elements to save.', 'elementor' ) );
}
$disabled_elements = json_decode( $elements );
if ( ! is_array( $disabled_elements ) ) {
wp_send_json_error( esc_html__( 'Unexpected elements data.', 'elementor' ) );
}
Options::update_disabled_elements( $disabled_elements );
do_action( 'elementor/element_manager/save_disabled_elements' );
wp_send_json_success();
}
public function ajax_get_widgets_usage() {
$this->verify_permission();
/** @var Usage_Module $usage_module */
$usage_module = Usage_Module::instance();
$usage_module->recalc_usage();
$widgets_usage = [];
foreach ( $usage_module->get_formatted_usage( 'raw' ) as $data ) {
foreach ( $data['elements'] as $element => $count ) {
if ( ! isset( $widgets_usage[ $element ] ) ) {
$widgets_usage[ $element ] = 0;
}
$widgets_usage[ $element ] += $count;
}
}
wp_send_json_success( $widgets_usage );
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace Elementor\Modules\ElementManager;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Admin\Menu\Admin_Menu_Manager;
use Elementor\Widget_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseModule {
const PAGE_ID = 'elementor-element-manager';
public function get_name() {
return 'element-manager';
}
public function __construct() {
parent::__construct();
$ajax = new Ajax();
$ajax->register_endpoints();
add_action( 'elementor/admin/menu/register', function( Admin_Menu_Manager $admin_menu ) {
$admin_menu->register( static::PAGE_ID, new Admin_Menu_App() );
}, 25 );
add_action( 'elementor/admin/menu/after_register', function ( Admin_Menu_Manager $admin_menu, array $hooks ) {
if ( ! empty( $hooks[ static::PAGE_ID ] ) ) {
add_action( "admin_print_scripts-{$hooks[ static::PAGE_ID ]}", [ $this, 'enqueue_assets' ] );
add_action( "admin_footer-{$hooks[ static::PAGE_ID ]}", [ $this, 'print_styles' ], 1000 );
}
}, 10, 2 );
add_filter( 'elementor/widgets/is_widget_enabled', function( $should_register, Widget_Base $widget_instance ) {
return ! Options::is_element_disabled( $widget_instance->get_name() );
}, 10, 2 );
add_filter( 'elementor/system-info/usage/settings', function( $usage ) {
$disabled_elements = Options::get_disabled_elements();
if ( ! empty( $disabled_elements ) ) {
$usage['disabled_elements'] = implode( ', ', $disabled_elements );
}
return $usage;
} );
add_filter( 'elementor/tracker/send_tracking_data_params', function( $params ) {
$disabled_elements = Options::get_disabled_elements();
if ( ! empty( $disabled_elements ) ) {
$params['usages']['disabled_elements'] = $disabled_elements;
}
return $params;
} );
}
public function enqueue_assets() {
wp_enqueue_script(
'e-element-manager-app',
$this->get_js_assets_url( 'element-manager-admin' ),
[
'wp-element',
'wp-components',
'wp-dom-ready',
'wp-i18n',
],
ELEMENTOR_VERSION
);
wp_localize_script( 'e-element-manager-app', 'eElementManagerConfig', [
'nonce' => wp_create_nonce( 'e-element-manager-app' ),
'ajaxurl' => admin_url( 'admin-ajax.php' ),
] );
wp_set_script_translations( 'e-element-manager-app', 'elementor' );
wp_enqueue_style( 'wp-components' );
wp_enqueue_style( 'wp-format-library' );
}
public function print_styles() {
?>
<style>
.components-button.is-secondary:disabled {
box-shadow: inset 0 0 0 1px #949494;
}
</style>
<?php
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Elementor\Modules\ElementManager;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Options {
public static function get_disabled_elements() {
return (array) get_option( 'elementor_disabled_elements', [] );
}
public static function update_disabled_elements( $elements ) {
update_option( 'elementor_disabled_elements', (array) $elements );
}
public static function is_element_disabled( $element_name ) {
return in_array( $element_name, self::get_disabled_elements() );
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Elementor\Modules\ElementsColorPicker;
use Elementor\Core\Experiments\Manager;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Experiments\Manager as Experiments_Manager;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseModule {
/**
* Retrieve the module name.
*
* @return string
*/
public function get_name() {
return 'elements-color-picker';
}
/**
* Enqueue the `Color-Thief` library to pick colors from images.
*
* @return void
*/
public function enqueue_scripts() {
wp_enqueue_script(
'color-thief',
$this->get_js_assets_url( 'color-thief', 'assets/lib/color-thief/', true ),
[ 'elementor-editor' ],
ELEMENTOR_VERSION,
true
);
}
/**
* Module constructor - Initialize the Eye-Dropper module.
*
* @return void
*/
public function __construct() {
add_action( 'elementor/editor/after_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Elementor\Modules\Favorites;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use Elementor\Data\V2\Base\Controller as Controller_Base;
use Elementor\Plugin;
class Controller extends Controller_Base {
public function get_name() {
return 'favorites';
}
public function create_item( $request ) {
$module = $this->get_module();
$type = $request->get_param( 'id' );
$favorite = $request->get_param( 'favorite' );
$module->update( $type, $favorite, $module::ACTION_MERGE );
return $module->get( $type );
}
public function delete_item( $request ) {
$module = $this->get_module();
$type = $request->get_param( 'id' );
$favorite = $request->get_param( 'favorite' );
$module->update( $type, $favorite, $module::ACTION_DELETE );
return $module->get( $type );
}
public function create_item_permissions_check( $request ) {
return current_user_can( 'edit_posts' );
}
public function delete_item_permissions_check( $request ) {
return $this->create_item_permissions_check( $request );
}
/**
* Get the favorites module instance.
*
* @return Module
*/
protected function get_module() {
return Plugin::instance()->modules_manager->get_modules( 'favorites' );
}
public function register_endpoints() {
$this->index_endpoint->register_item_route( \WP_REST_Server::CREATABLE, [
'id_arg_type_regex' => '[\w]+',
'id' => [
'description' => 'Type of favorites.',
'type' => 'string',
'required' => true,
],
'favorite' => [
'description' => 'The favorite slug to create.',
'type' => 'string',
'required' => true,
],
] );
$this->index_endpoint->register_item_route( \WP_REST_Server::DELETABLE, [
'id_arg_type_regex' => '[\w]+',
'id' => [
'description' => 'Type of favorites.',
'type' => 'string',
'required' => true,
],
'favorite' => [
'description' => 'The favorite slug to delete.',
'type' => 'string',
'required' => true,
],
] );
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Elementor\Modules\Favorites;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use Elementor\Core\Utils\Collection;
use Elementor\Core\Utils\Static_Collection;
abstract class Favorites_Type extends Static_Collection {
public function __construct( array $items = [] ) {
parent::__construct( $items, true );
}
/**
* Get the name of the type.
*
* @return mixed
*/
abstract public function get_name();
/**
* Prepare favorites before taking any action.
*
* @param Collection|array|string $favorites
*
* @return array
*/
public function prepare( $favorites ) {
if ( $favorites instanceof Collection ) {
$favorites = $favorites->values();
}
if ( ! is_array( $favorites ) ) {
return [ $favorites ];
}
return $favorites;
}
}

View File

@@ -0,0 +1,248 @@
<?php
namespace Elementor\Modules\Favorites;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Experiments\Manager;
use Elementor\Modules\Favorites\Types\Widgets;
use Elementor\Plugin;
use http\Exception\InvalidArgumentException;
use WP_Error;
class Module extends BaseModule {
/**
* List of registered favorites type.
*
* @var Favorites_Type[]
*/
protected $types = [];
const OPTION_NAME = 'elementor_editor_user_favorites';
/**
* The name of the merge action.
*
* @var string
*/
const ACTION_MERGE = 'merge';
/**
* The name of the delete action.
*
* @var string
*/
const ACTION_DELETE = 'delete';
/**
* Favorites module constructor.
*/
public function __construct() {
// Register default types
$this->register( Widgets::class );
$this->populate();
Plugin::instance()->data_manager_v2->register_controller( new Controller() );
add_filter( 'elementor/tracker/send_tracking_data_params', [ $this, 'add_tracking_data' ] );
}
/**
* Add usage data related to favorites.
*
* @param $params
*
* @return array
*/
public function add_tracking_data( $params ) {
$params['usages']['favorites'] = $this->get();
return $params;
}
public function get_name() {
return 'favorites';
}
/**
* Get user favorites by type.
*
* @param string[]|string $type
*
* @return array
*/
public function get( $type = null ) {
if ( null === $type ) {
$type = array_keys( $this->types );
}
if ( is_array( $type ) ) {
return array_intersect_key(
$this->combined(),
array_flip( (array) $type )
);
}
return $this->type_instance( $type )
->values();
}
/**
* Merge new user favorites to a type.
*
* @param string $type
* @param array|string $favorites
* @param bool $store
*
* @return array|bool
*/
public function merge( $type, $favorites, $store = true ) {
return $this->update( $type, $favorites, static::ACTION_MERGE, $store );
}
/**
* Delete existing favorites from a type.
*
* @param string $type
* @param array|string $favorites
* @param bool $store
*
* @return array|int
*/
public function delete( $type, $favorites, $store = true ) {
return $this->update( $type, $favorites, static::ACTION_DELETE, $store );
}
/**
* Update favorites on a type by merging or deleting from it.
*
* @param $type
* @param $favorites
* @param $action
* @param bool $store
*
* @return array|boolean
*/
public function update( $type, $favorites, $action, $store = true ) {
$type_instance = $this->type_instance( $type );
$favorites = $type_instance->prepare( $favorites );
switch ( $action ) {
case static::ACTION_MERGE:
$type_instance->merge( $favorites );
break;
case static::ACTION_DELETE:
$type_instance->filter(
function( $value ) use ( $favorites ) {
return ! in_array( $value, $favorites, true );
}
);
break;
default:
$this->action_doesnt_exists( $action );
}
if ( $store && ! $this->store() ) {
return false;
}
return $type_instance->values();
}
/**
* Get registered favorites type instance.
*
* @param string $type
*
* @return Favorites_Type
*/
public function type_instance( $type ) {
return $this->types[ $type ];
}
/**
* Register a new type class.
*
* @param string $class
*/
public function register( $class ) {
$type_instance = new $class();
$this->types[ $type_instance->get_name() ] = $type_instance;
}
/**
* Returns all available types keys.
*
* @return string[]
*/
public function available() {
return array_keys( $this->types );
}
/**
* Combine favorites from all types into a single array.
*
* @return array
*/
protected function combined() {
$all = [];
foreach ( $this->types as $type ) {
$favorites = $type->values();
if ( ! empty( $favorites ) ) {
$all[ $type->get_name() ] = $favorites;
}
}
return $all;
}
/**
* Populate all type classes with the stored data.
*/
protected function populate() {
$combined = $this->retrieve();
foreach ( $this->types as $key => $type ) {
if ( isset( $combined[ $key ] ) ) {
$type->merge( $combined[ $key ] );
}
}
}
/**
* Retrieve stored user favorites types.
*
* @return mixed|false
*/
protected function retrieve() {
return get_user_option( static::OPTION_NAME );
}
/**
* Update all changes to user favorites type.
*
* @return int|bool
*/
protected function store() {
return update_user_option( get_current_user_id(), static::OPTION_NAME, $this->combined() );
}
/**
* Throw action doesn't exist exception.
*
* @param string $action
*/
public function action_doesnt_exists( $action ) {
throw new \InvalidArgumentException( sprintf(
"Action '%s' to apply on favorites doesn't exists",
$action
) );
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Elementor\Modules\Favorites\Types;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use Elementor\Modules\Favorites\Favorites_Type;
use Elementor\Plugin;
class Widgets extends Favorites_Type {
const CATEGORY_SLUG = 'favorites';
/**
* Widgets favorites type constructor.
*/
public function __construct( array $items = [] ) {
parent::__construct( $items );
add_action( 'elementor/document/before_get_config', [ $this, 'update_widget_categories' ], 10, 1 );
}
public function get_name() {
return 'widgets';
}
public function prepare( $favorites ) {
return array_intersect( parent::prepare( $favorites ), $this->get_available() );
}
/**
* Get all available widgets.
*
* @return string[]
*/
public function get_available() {
return array_merge(
array_keys(
Plugin::instance()->widgets_manager->get_widget_types()
),
array_keys(
Plugin::instance()->elements_manager->get_element_types()
)
);
}
/**
* Update the categories of a widget inside a filter.
*
* @param $document
*/
public function update_widget_categories( $document ) {
foreach ( $this->values() as $favorite ) {
$widget = Plugin::$instance->widgets_manager->get_widget_types( $favorite );
// If it's not a widget, maybe it's an element.
if ( ! $widget ) {
$widget = Plugin::$instance->elements_manager->get_element_types( $favorite );
}
if ( $widget ) {
$widget->set_config( 'categories', [ static::CATEGORY_SLUG ] );
}
}
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Elementor\Modules\FloatingButtons\AdminMenuItems;
use Elementor\Core\Admin\Menu\Interfaces\Admin_Menu_Item_With_Page;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Floating_Buttons_Empty_View_Menu_Item extends Floating_Buttons_Menu_Item implements Admin_Menu_Item_With_Page {
private $render_callback;
public function __construct( callable $render_callback ) {
$this->render_callback = $render_callback;
}
public function render() {
( $this->render_callback )();
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Elementor\Modules\FloatingButtons\AdminMenuItems;
use Elementor\Core\Admin\Menu\Interfaces\Admin_Menu_Item;
use Elementor\TemplateLibrary\Source_Local;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Floating_Buttons_Menu_Item implements Admin_Menu_Item {
public function is_visible() {
return true;
}
public function get_parent_slug() {
return Source_Local::ADMIN_MENU_SLUG;
}
public function get_label() {
return esc_html__( 'Floating Elements', 'elementor' );
}
public function get_page_title() {
return esc_html__( 'Floating Elements', 'elementor' );
}
public function get_capability() {
return 'manage_options';
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Elementor\Modules\FloatingButtons\Classes\Render;
/**
* Class Contact_Buttons_Core_Render.
*
* This class handles the rendering of the Contact Buttons widget for the core version.
*
* @since 3.23.0
*/
class Contact_Buttons_Core_Render extends Contact_Buttons_Render_Base {
public function render(): void {
$this->build_layout_render_attribute();
$this->add_content_wrapper_render_attribute();
$content_classnames = 'e-contact-buttons__content';
$animation_duration = $this->settings['style_chat_box_animation_duration'];
if ( ! empty( $animation_duration ) ) {
$content_classnames .= ' has-animation-duration-' . $animation_duration;
}
$this->widget->add_render_attribute( 'content', [
'class' => $content_classnames,
] );
?>
<div <?php echo $this->widget->get_render_attribute_string( 'layout' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<div <?php echo $this->widget->get_render_attribute_string( 'content-wrapper' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<div <?php echo $this->widget->get_render_attribute_string( 'content' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<?php
$this->render_top_bar();
$this->render_message_bubble();
$this->render_send_button();
?>
</div>
</div>
<?php
$this->render_chat_button();
?>
</div>
<?php
}
protected function add_layout_render_attribute( $layout_classnames ) {
$this->widget->add_render_attribute( 'layout', [
'class' => $layout_classnames,
'id' => $this->settings['advanced_custom_css_id'],
'data-document-id' => get_the_ID(),
'aria-role' => 'dialog',
] );
}
protected function add_content_wrapper_render_attribute() {
$this->widget->add_render_attribute( 'content-wrapper', [
'aria-hidden' => 'true',
'aria-label' => __( 'Links window', 'elementor' ),
'class' => 'e-contact-buttons__content-wrapper hidden',
'id' => 'e-contact-buttons__content-wrapper',
] );
}
}

View File

@@ -0,0 +1,487 @@
<?php
namespace Elementor\Modules\FloatingButtons\Classes\Render;
use Elementor\Core\Base\Providers\Social_Network_Provider;
use Elementor\Icons_Manager;
use Elementor\Modules\FloatingButtons\Base\Widget_Contact_Button_Base;
use Elementor\Utils;
/**
* Class Contact_Buttons_Render_Base.
*
* This is the base class that will hold shared functionality that will be needed by all the various widget versions.
*
* @since 3.23.0
*/
abstract class Contact_Buttons_Render_Base {
protected Widget_Contact_Button_Base $widget;
protected array $settings;
abstract public function render(): void;
public function __construct( Widget_Contact_Button_Base $widget ) {
$this->widget = $widget;
$this->settings = $widget->get_settings_for_display();
}
protected function render_chat_button_icon(): void {
$platform = $this->settings['chat_button_platform'] ?? '';
$mapping = Social_Network_Provider::get_icon_mapping( $platform );
$icon_lib = explode( ' ', $mapping )[0];
$library = 'fab' === $icon_lib ? 'fa-brands' : 'fa-solid';
Icons_Manager::render_icon(
[
'library' => $library,
'value' => $mapping,
],
[ 'aria-hidden' => 'true' ]
);
}
protected function render_chat_button(): void {
$platform = $this->settings['chat_button_platform'] ?? '';
$display_dot = $this->settings['chat_button_show_dot'] ?? '';
$button_size = $this->settings['style_chat_button_size'];
$hover_animation = $this->settings['style_button_color_hover_animation'];
$entrance_animation = $this->settings['style_chat_button_animation'];
$entrance_animation_duration = $this->settings['style_chat_button_animation_duration'];
$entrance_animation_delay = $this->settings['style_chat_button_animation_delay'];
$accessible_name = $this->settings['chat_aria_label'];
$button_classnames = 'e-contact-buttons__chat-button e-contact-buttons__chat-button-shadow';
if ( ! empty( $button_size ) ) {
$button_classnames .= ' has-size-' . $button_size;
}
if ( ! empty( $hover_animation ) ) {
$button_classnames .= ' elementor-animation-' . $hover_animation;
}
if ( ! empty( $entrance_animation ) && 'none' != $entrance_animation ) {
$button_classnames .= ' has-entrance-animation';
}
if ( ! empty( $entrance_animation_delay ) ) {
$button_classnames .= ' has-entrance-animation-delay';
}
if ( ! empty( $entrance_animation_duration ) ) {
$button_classnames .= ' has-entrance-animation-duration-' . $entrance_animation_duration;
}
if ( 'yes' === $display_dot ) {
$button_classnames .= ' has-dot';
}
$this->widget->add_render_attribute( 'button', [
'class' => $button_classnames,
'aria-controls' => 'e-contact-buttons__content-wrapper',
'aria-label' => sprintf(
/* translators: 1: Accessible name. */
esc_html__( 'Toggle %1$s', 'elementor' ),
$accessible_name,
),
'type' => 'button',
] );
?>
<div class="e-contact-buttons__chat-button-container">
<button <?php echo $this->widget->get_render_attribute_string( 'button' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<?php
$this->render_chat_button_icon();
?>
</button>
</div>
<?php
}
protected function render_close_button(): void {
$accessible_name = $this->settings['chat_aria_label'];
$this->widget->add_render_attribute( 'close-button', [
'class' => 'e-contact-buttons__close-button',
'aria-controls' => 'e-contact-buttons__content-wrapper',
'aria-label' => sprintf(
/* translators: 1: Accessible name. */
esc_html__( 'Close %1$s', 'elementor' ),
$accessible_name,
),
'type' => 'button',
] );
?>
<button <?php echo $this->widget->get_render_attribute_string( 'close-button' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<i class="eicon-close"></i>
</button>
<?php
}
protected function render_top_bar(): void {
$profile_image_value = $this->settings['top_bar_image'] ?? [];
$has_profile_image = ! empty( $profile_image_value ) && ( ! empty( $profile_image_value['url'] || ! empty( $profile_image_value['id'] ) ) );
$profile_image_size = $this->settings['style_top_bar_image_size'];
$display_profile_dot = $this->settings['top_bar_show_dot'];
$profile_image_classnames = 'e-contact-buttons__profile-image';
if ( ! empty( $profile_image_size ) ) {
$profile_image_classnames .= ' has-size-' . $profile_image_size;
}
if ( 'yes' === $display_profile_dot ) {
$profile_image_classnames .= ' has-dot';
}
$top_bar_title = $this->settings['top_bar_title'] ?? '';
$top_bar_subtitle = $this->settings['top_bar_subtitle'] ?? '';
$has_top_bar_title = ! empty( $top_bar_title );
$has_top_bar_subtitle = ! empty( $top_bar_subtitle );
$this->widget->add_render_attribute( 'profile-image', [
'class' => $profile_image_classnames,
] );
?>
<div class="e-contact-buttons__top-bar">
<?php $this->render_close_button(); ?>
<div <?php echo $this->widget->get_render_attribute_string( 'profile-image' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<?php if ( ! empty( $profile_image_value['id'] ) ) {
echo wp_get_attachment_image( $profile_image_value['id'], 'medium', false, [
'class' => 'e-contact-buttons__profile-image-el',
] );
} else {
$this->widget->add_render_attribute( 'profile-image-src', [
'alt' => '',
'class' => 'e-contact-buttons__profile-image-el',
'src' => esc_url( $profile_image_value['url'] ),
] );
?>
<img <?php echo $this->widget->get_render_attribute_string( 'profile-image-src' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> />
<?php }; ?>
</div>
<div class="e-contact-buttons__top-bar-details">
<?php if ( $has_top_bar_title ) { ?>
<p class="e-contact-buttons__top-bar-title"><?php echo esc_html( $top_bar_title ); ?></p>
<?php } ?>
<?php if ( $has_top_bar_subtitle ) { ?>
<p class="e-contact-buttons__top-bar-subtitle"><?php echo esc_html( $top_bar_subtitle ); ?></p>
<?php } ?>
</div>
</div>
<?php
}
protected function render_message_bubble_typing_animation(): void {
$has_typing_animation = 'yes' === $this->settings['chat_button_show_animation'];
?>
<?php if ( $has_typing_animation ) { ?>
<div class="e-contact-buttons__dots-container">
<span class="e-contact-buttons__dot e-contact-buttons__dot-1"></span>
<span class="e-contact-buttons__dot e-contact-buttons__dot-2"></span>
<span class="e-contact-buttons__dot e-contact-buttons__dot-3"></span>
</div>
<?php } ?>
<?php
}
protected function render_message_bubble_container(): void {
$message_bubble_name = $this->settings['message_bubble_name'] ?? '';
$message_bubble_body = $this->settings['message_bubble_body'] ?? '';
$has_message_bubble_name = ! empty( $message_bubble_name );
$has_message_bubble_body = ! empty( $message_bubble_body );
$time_format = $this->settings['chat_button_time_format'];
?>
<div class="e-contact-buttons__bubble-container">
<div class="e-contact-buttons__bubble">
<?php if ( $has_message_bubble_name ) { ?>
<p class="e-contact-buttons__message-bubble-name"><?php echo esc_html( $message_bubble_name ); ?></p>
<?php } ?>
<?php if ( $has_message_bubble_body ) { ?>
<p class="e-contact-buttons__message-bubble-body"><?php echo esc_html( $message_bubble_body ); ?></p>
<?php } ?>
<p class="e-contact-buttons__message-bubble-time" data-time-format="<?php echo esc_attr( $time_format ); ?>"></p>
</div>
</div>
<?php
}
protected function render_message_bubble_powered_by(): void {
if ( Utils::has_pro() ) {
return;
}
?>
<div class="e-contact-buttons__powered-container">
<p class="e-contact-buttons__powered-text">
<?php echo esc_attr__( 'Powered by Elementor', 'elementor' ); ?>
</p>
</div>
<?php
}
protected function render_message_bubble(): void {
$message_bubble_classnames = 'e-contact-buttons__message-bubble';
$show_animation = $this->settings['chat_button_show_animation'] ?? false;
$has_typing_animation = $show_animation && 'yes' === $show_animation;
if ( $has_typing_animation ) {
$message_bubble_classnames .= ' has-typing-animation';
}
$this->widget->add_render_attribute( 'message-bubble', [
'class' => $message_bubble_classnames,
] );
?>
<div <?php echo $this->widget->get_render_attribute_string( 'message-bubble' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<?php
$this->render_message_bubble_typing_animation();
$this->render_message_bubble_container();
?>
</div>
<?php
}
protected function render_contact_text(): void {
$contact_cta_text = $this->settings['contact_cta_text'] ?? '';
?>
<?php if ( ! empty( $contact_cta_text ) ) { ?>
<p class="e-contact-buttons__contact-text"><?php echo esc_html( $contact_cta_text ); ?></p>
<?php } ?>
<?php
}
protected function render_contact_links(): void {
$contact_icons = $this->settings['contact_repeater'] ?? [];
$icons_size = $this->settings['style_contact_button_size'] ?? 'small';
$hover_animation = $this->settings['style_contact_button_hover_animation'];
?>
<div class="e-contact-buttons__contact-links">
<?php
foreach ( $contact_icons as $key => $icon ) {
$icon_text_mapping = Social_Network_Provider::get_text_mapping( $icon['contact_icon_platform'] );
$aria_label = sprintf(
/* translators: 1: Platform name. */
esc_html__( 'Open %1$s', 'elementor' ),
$icon_text_mapping,
);
$link = [
'platform' => $icon['contact_icon_platform'],
'number' => $icon['contact_icon_number'] ?? '',
'username' => $icon['contact_icon_username'] ?? '',
'email_data' => [
'contact_icon_mail' => $icon['contact_icon_mail'] ?? '',
'contact_icon_mail_subject' => $icon['contact_icon_mail_subject'] ?? '',
'contact_icon_mail_body' => $icon['contact_icon_mail_body'] ?? '',
],
'viber_action' => $icon['contact_icon_viber_action'] ?? '',
];
$formatted_link = $this->get_formatted_link( $link, 'contact_icon' );
$icon_classnames = 'e-contact-buttons__contact-icon-link has-size-' . $icons_size;
if ( ! empty( $hover_animation ) ) {
$icon_classnames .= ' elementor-animation-' . $hover_animation;
}
$this->widget->add_render_attribute( 'icon-link-' . $key, [
'aria-label' => $aria_label,
'class' => $icon_classnames,
'href' => $formatted_link,
'rel' => 'noopener noreferrer',
'target' => '_blank',
] );
?>
<a <?php echo $this->widget->get_render_attribute_string( 'icon-link-' . $key ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<?php
$mapping = Social_Network_Provider::get_icon_mapping( $icon['contact_icon_platform'] );
$icon_lib = explode( ' ', $mapping )[0];
$library = 'fab' === $icon_lib ? 'fa-brands' : 'fa-solid';
Icons_Manager::render_icon(
[
'library' => $library,
'value' => $mapping,
],
[ 'aria-hidden' => 'true' ]
);
?>
</a>
<?php } ?>
</div>
<?php
}
protected function render_contact_section(): void {
?>
<div class="e-contact-buttons__contact">
<?php
$this->render_contact_text();
$this->render_contact_links();
?>
</div>
<?php
}
protected function render_send_button(): void {
$platform = $this->settings['chat_button_platform'] ?? '';
$send_button_text = $this->settings['send_button_text'];
$hover_animation = $this->settings['style_send_hover_animation'];
$cta_classnames = 'e-contact-buttons__send-cta';
$link = [
'platform' => $platform,
'number' => $this->settings['chat_button_number'] ?? '',
'username' => $this->settings['chat_button_username'] ?? '',
'email_data' => [
'chat_button_mail' => $this->settings['chat_button_mail'],
'chat_button_mail_subject' => $this->settings['chat_button_mail_subject'] ?? '',
'chat_button_mail_body' => $this->settings['chat_button_mail_body'] ?? '',
],
'viber_action' => $this->settings['chat_button_viber_action'],
];
$formatted_link = $this->get_formatted_link( $link, 'chat_button' );
if ( ! empty( $hover_animation ) ) {
$cta_classnames .= ' elementor-animation-' . $hover_animation;
}
$this->widget->add_render_attribute( 'formatted-cta', [
'class' => $cta_classnames,
'href' => $formatted_link,
'rel' => 'noopener noreferrer',
'target' => '_blank',
] );
?>
<div class="e-contact-buttons__send-button">
<?php $this->render_message_bubble_powered_by(); ?>
<div class="e-contact-buttons__send-button-container">
<?php if ( $send_button_text ) { ?>
<a <?php echo $this->widget->get_render_attribute_string( 'formatted-cta' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<?php
$mapping = Social_Network_Provider::get_icon_mapping( $platform );
$icon_lib = explode( ' ', $mapping )[0];
$library = 'fab' === $icon_lib ? 'fa-brands' : 'fa-solid';
Icons_Manager::render_icon(
[
'library' => $library,
'value' => $mapping,
],
[ 'aria-hidden' => 'true' ]
);
?>
<?php echo esc_html( $send_button_text ); ?>
</a>
<?php } ?>
</div>
</div>
<?php
}
protected function get_formatted_link( array $link, string $prefix ): string {
// Ensure we clear the default link value if the matching type value is empty
switch ( $link['platform'] ) {
case Social_Network_Provider::EMAIL:
$formatted_link = Social_Network_Provider::build_email_link( $link['email_data'], $prefix );
break;
case Social_Network_Provider::SMS:
$formatted_link = ! empty( $link['number'] ) ? 'sms:' . $link['number'] : '';
break;
case Social_Network_Provider::MESSENGER:
$formatted_link = ! empty( $link['username'] ) ?
Social_Network_Provider::build_messenger_link( $link['username'] ) :
'';
break;
case Social_Network_Provider::WHATSAPP:
$formatted_link = ! empty( $link['number'] ) ? 'https://wa.me/' . $link['number'] : '';
break;
case Social_Network_Provider::VIBER:
$formatted_link = Social_Network_Provider::build_viber_link( $link['viber_action'], $link['number'] );
break;
case Social_Network_Provider::SKYPE:
$formatted_link = ! empty( $link['username'] ) ? 'skype:' . $link['username'] . '?chat' : '';
break;
case Social_Network_Provider::TELEPHONE:
$formatted_link = ! empty( $link['number'] ) ? 'tel:' . $link['number'] : '';
break;
default:
break;
}
return esc_html( $formatted_link );
}
protected function is_url_link( string $platform ): bool {
return Social_Network_Provider::URL === $platform || Social_Network_Provider::WAZE === $platform;
}
protected function render_link_attributes( array $link, string $key ) {
switch ( $link['platform'] ) {
case Social_Network_Provider::WAZE:
if ( empty( $link['location']['url'] ) ) {
$link['location']['url'] = '#';
}
$this->widget->add_link_attributes( $key, $link['location'] );
break;
case Social_Network_Provider::URL:
if ( empty( $link['url']['url'] ) ) {
$link['url']['url'] = '#';
}
$this->widget->add_link_attributes( $key, $link['url'] );
break;
default:
break;
}
}
protected function build_layout_render_attribute(): void {
$layout_classnames = 'e-contact-buttons e-' . $this->widget->get_name();
$platform = $this->settings['chat_button_platform'] ?? '';
$border_radius = $this->settings['style_chat_box_corners'];
$alignment_position_horizontal = $this->settings['advanced_horizontal_position'];
$alignment_position_vertical = $this->settings['advanced_vertical_position'];
$has_animations = ! empty( $this->settings['style_chat_box_exit_animation'] ) || ! empty( $this->settings['style_chat_box_entrance_animation'] );
$custom_classes = $this->settings['advanced_custom_css_classes'] ?? '';
$icon_name_mapping = Social_Network_Provider::get_name_mapping( $platform );
if ( ! empty( $platform ) ) {
$layout_classnames .= ' has-platform-' . $icon_name_mapping;
}
if ( ! empty( $border_radius ) ) {
$layout_classnames .= ' has-corners-' . $border_radius;
}
if ( ! empty( $alignment_position_horizontal ) ) {
$layout_classnames .= ' has-h-alignment-' . $alignment_position_horizontal;
}
if ( ! empty( $alignment_position_vertical ) ) {
$layout_classnames .= ' has-v-alignment-' . $alignment_position_vertical;
}
if ( $has_animations ) {
$layout_classnames .= ' has-animations';
}
if ( $custom_classes ) {
$layout_classnames .= ' ' . $custom_classes;
}
$this->add_layout_render_attribute( $layout_classnames );
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace Elementor\Modules\FloatingButtons\Classes\Render;
use Elementor\Icons_Manager;
/**
* Class Floating_Bars_Core_Render.
*
* This class handles the rendering of the Floating Bars widget for the core version.
*
* @since 3.23.0
*/
class Floating_Bars_Core_Render extends Floating_Bars_Render_Base {
protected function render_announcement_icon(): void {
$icon = $this->settings['announcement_icon'] ?? '';
if ( '' !== $icon['value'] ) : ?>
<span class="e-floating-bars__announcement-icon"><?php Icons_Manager::render_icon( $icon, [ 'aria-hidden' => 'true' ] ); ?></span>
<?php endif;
}
protected function render_announcement_text(): void {
$text = $this->settings['announcement_text'] ?? '';
$this->widget->add_render_attribute( 'announcement_text', [
'class' => 'e-floating-bars__announcement-text',
] );
if ( '' !== $text ) : ?>
<p <?php $this->widget->print_render_attribute_string( 'announcement_text' ); ?>>
<?php echo esc_html( $text ); ?>
</p>
<?php endif;
}
protected function render_cta_icon(): void {
$icon = $this->settings['cta_icon'] ?? '';
$icon_classnames = 'e-floating-bars__cta-icon';
$this->widget->add_render_attribute( 'cta-icon', [
'class' => $icon_classnames,
] );
if ( '' !== $icon['value'] ) : ?>
<span <?php echo $this->widget->get_render_attribute_string( 'cta-icon' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>><?php Icons_Manager::render_icon( $icon, [ 'aria-hidden' => 'true' ] ); ?></span>
<?php endif;
}
protected function render_cta_button(): void {
$link = $this->settings['cta_link'] ?? '';
$text = $this->settings['cta_text'] ?? '';
$hover_animation = $this->settings['style_cta_button_hover_animation'];
$corners = $this->settings['style_cta_button_corners'];
$link_type = $this->settings['style_cta_type'];
$entrance_animation = $this->settings['style_cta_button_animation'];
$has_border = $this->settings['style_cta_button_show_border'];
$cta_classnames = 'e-floating-bars__cta-button';
if ( ! empty( $hover_animation ) ) {
$cta_classnames .= ' elementor-animation-' . $hover_animation;
}
if ( ! empty( $corners ) ) {
$cta_classnames .= ' has-corners-' . $corners;
}
if ( ! empty( $link_type ) ) {
$cta_classnames .= ' is-type-' . $link_type;
}
if ( ! empty( $entrance_animation ) && 'none' != $entrance_animation ) {
$cta_classnames .= ' has-entrance-animation';
}
if ( 'yes' == $has_border ) {
$cta_classnames .= ' has-border';
}
$this->widget->add_render_attribute( 'cta-button', [
'class' => $cta_classnames,
] );
$this->widget->add_render_attribute( 'cta_text', [
'class' => 'e-floating-bars__cta-text',
] );
if ( ! empty( $text ) ) {
$this->widget->add_link_attributes( 'cta-button', $link );
?>
<div class="e-floating-bars__cta-button-container">
<a <?php echo $this->widget->get_render_attribute_string( 'cta-button' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<?php $this->render_cta_icon(); ?>
<span <?php $this->widget->print_render_attribute_string( 'cta_text' ); ?>><?php echo esc_html( $text ); ?></span>
</a>
</div>
<?php
}
}
protected function render_close_button(): void {
$accessible_name = $this->settings['accessible_name'];
$close_button_classnames = 'e-floating-bars__close-button';
$this->widget->add_render_attribute( 'close-button', [
'class' => $close_button_classnames,
'aria-label' => sprintf(
/* translators: 1: Accessible name. */
esc_html__( 'Close %1$s', 'elementor' ),
$accessible_name,
),
'type' => 'button',
'aria-controls' => 'e-floating-bars',
] );
?>
<button <?php echo $this->widget->get_render_attribute_string( 'close-button' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<i class="eicon-close"></i>
</button>
<?php
}
public function render(): void {
$this->build_layout_render_attribute();
$has_close_button = $this->settings['floating_bar_close_switch'];
?>
<div <?php echo $this->widget->get_render_attribute_string( 'layout' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<?php
$this->render_announcement_text();
$this->render_announcement_icon();
$this->render_cta_button();
if ( 'yes' === $has_close_button ) {
$this->render_close_button();
}
?>
<div class="e-floating-bars__overlay"></div>
</div>
<?php
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Elementor\Modules\FloatingButtons\Classes\Render;
use Elementor\Modules\FloatingButtons\Base\Widget_Floating_Bars_Base;
/**
* Class Floating_Bars_Render_Base.
*
* This is the base class that will hold shared functionality that will be needed by all the various widget versions.
*
* @since 3.23.0
*/
abstract class Floating_Bars_Render_Base {
protected Widget_Floating_Bars_Base $widget;
protected array $settings;
abstract public function render(): void;
public function __construct( Widget_Floating_Bars_Base $widget ) {
$this->widget = $widget;
$this->settings = $widget->get_settings_for_display();
}
protected function add_layout_render_attribute( $layout_classnames ) {
$this->widget->add_render_attribute( 'layout', [
'class' => $layout_classnames,
'id' => $this->settings['advanced_custom_css_id'],
'data-document-id' => get_the_ID(),
'role' => 'alertdialog',
] );
}
public static function get_layout_classnames( Widget_Floating_Bars_Base $widget, array $settings ): string {
$layout_classnames = 'e-floating-bars e-' . $widget->get_name();
$vertical_position = $settings['advanced_vertical_position'];
$is_sticky = $settings['advanced_toggle_sticky'];
$has_close_button = $settings['floating_bar_close_switch'];
$layout_classnames .= ' has-vertical-position-' . $vertical_position;
if ( 'yes' === $has_close_button ) {
$layout_classnames .= ' has-close-button';
}
if ( 'yes' === $is_sticky ) {
$layout_classnames .= ' is-sticky';
}
return $layout_classnames;
}
protected function build_layout_render_attribute(): void {
$layout_classnames = static::get_layout_classnames( $this->widget, $this->settings );
$this->add_layout_render_attribute( $layout_classnames );
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Elementor\Modules\FloatingButtons\Control;
use Elementor\Control_Hover_Animation;
class Hover_Animation_Floating_Buttons extends Control_Hover_Animation {
const TYPE = 'hover_animation_contact_buttons';
public function get_type() {
return static::TYPE;
}
public static function get_animations() {
return [
'grow' => 'Grow',
'pulse' => 'Pulse',
'push' => 'Push',
'float' => 'Float',
];
}
}

View File

@@ -0,0 +1,254 @@
<?php
namespace Elementor\Modules\FloatingButtons\Documents;
use Elementor\Core\Base\Document;
use Elementor\Core\DocumentTypes\PageBase;
use Elementor\Modules\FloatingButtons\Module;
use Elementor\Modules\Library\Traits\Library as Library_Trait;
use Elementor\Modules\FloatingButtons\Module as Floating_Buttons_Module;
use Elementor\Modules\PageTemplates\Module as Page_Templates_Module;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Utils as ElementorUtils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Floating_Buttons extends PageBase {
use Library_Trait;
public static function get_properties() {
$properties = parent::get_properties();
$properties['support_kit'] = true;
$properties['support_site_editor'] = false;
$properties['cpt'] = [ Floating_Buttons_Module::CPT_FLOATING_BUTTONS ];
$properties['show_navigator'] = false;
$properties['allow_adding_widgets'] = false;
$properties['support_page_layout'] = false;
$properties['library_close_title'] = esc_html__( 'Go To Dashboard', 'elementor' );
$properties['publish_button_title'] = esc_html__( 'After publishing this widget, you will be able to set it as visible on the entire site in the Admin Table.', 'elementor' );
$properties['allow_closing_remote_library'] = false;
return $properties;
}
public static function get_floating_element_type( $post_id ) {
$meta = get_post_meta( $post_id, Floating_Buttons_Module::FLOATING_ELEMENTS_TYPE_META_KEY, true );
return $meta ? $meta : 'floating-buttons';
}
public static function is_editing_existing_floating_buttons_page() {
$action = ElementorUtils::get_super_global_value( $_GET, 'action' );
$post_id = ElementorUtils::get_super_global_value( $_GET, 'post' );
return 'elementor' === $action && static::is_floating_buttons_type_meta_key( $post_id );
}
public static function is_creating_floating_buttons_page() {
$action = ElementorUtils::get_super_global_value( $_POST, 'action' ); //phpcs:ignore WordPress.Security.NonceVerification.Missing
$post_id = ElementorUtils::get_super_global_value( $_POST, 'editor_post_id' ); //phpcs:ignore WordPress.Security.NonceVerification.Missing
return 'elementor_ajax' === $action && static::is_floating_buttons_type_meta_key( $post_id );
}
public static function is_floating_buttons_type_meta_key( $post_id ) {
return Module::FLOATING_BUTTONS_DOCUMENT_TYPE === get_post_meta( $post_id, Document::TYPE_META_KEY, true );
}
public function print_content() {
$plugin = \Elementor\Plugin::$instance;
if ( $plugin->preview->is_preview_mode( $this->get_main_id() ) ) {
// PHPCS - the method builder_wrapper is safe.
echo $plugin->preview->builder_wrapper( '' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} else {
// PHPCS - the method get_content is safe.
echo $this->get_content(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
}
public function get_location() {
return self::get_property( 'location' );
}
public static function get_type() {
return Floating_Buttons_Module::FLOATING_BUTTONS_DOCUMENT_TYPE;
}
public static function register_post_fields_control( $document ) {}
public static function register_hide_title_control( $document ) {}
public function get_name() {
return Floating_Buttons_Module::FLOATING_BUTTONS_DOCUMENT_TYPE;
}
public function filter_admin_row_actions( $actions ) {
unset( $actions['edit'] );
unset( $actions['inline hide-if-no-js'] );
$built_with_elementor = parent::filter_admin_row_actions( [] );
if ( isset( $actions['trash'] ) ) {
$delete = $actions['trash'];
unset( $actions['trash'] );
$actions['trash'] = $delete;
}
if ( 'publish' === $this->get_post()->post_status ) {
$actions = $this->set_as_entire_site( $actions );
}
return $built_with_elementor + $actions;
}
public static function get_meta_query_for_floating_buttons( string $floating_element_type ): array {
$meta_query = [
'relation' => 'AND',
[
'key' => '_elementor_conditions',
'compare' => 'EXISTS',
],
];
if ( 'floating-buttons' === $floating_element_type ) {
$meta_query[] = [
'relation' => 'OR',
[
'key' => Module::FLOATING_ELEMENTS_TYPE_META_KEY,
'compare' => 'NOT EXISTS',
],
[
'key' => Module::FLOATING_ELEMENTS_TYPE_META_KEY,
'value' => 'floating-buttons',
],
];
} else {
$meta_query[] = [
'key' => Module::FLOATING_ELEMENTS_TYPE_META_KEY,
'value' => $floating_element_type,
];
}
return $meta_query;
}
/**
* Tries to find the post id of the floating element that is set as entire site.
* If found, returns the post id, otherwise returns 0.
*
* @param string $floating_element_type
*
* @return int
*/
public static function get_set_as_entire_site_post_id( string $floating_element_type ): int {
static $types = [];
if ( isset( $types[ $floating_element_type ] ) ) {
return $types[ $floating_element_type ];
}
$query = new \WP_Query( [
'post_type' => Floating_Buttons_Module::CPT_FLOATING_BUTTONS,
'posts_per_page' => -1,
'post_status' => 'publish',
'fields' => 'ids',
'no_found_rows' => true,
'update_post_term_cache' => false,
'meta_query' => static::get_meta_query_for_floating_buttons( $floating_element_type ),
] );
foreach ( $query->get_posts() as $post_id ) {
$conditions = get_post_meta( $post_id, '_elementor_conditions', true );
if ( ! $conditions ) {
continue;
}
if ( in_array( 'include/general', $conditions ) ) {
$types[ $floating_element_type ] = $post_id;
return $post_id;
}
}
return 0;
}
public function set_as_entire_site( $actions ) {
$floating_element_type = static::get_floating_element_type( $this->get_main_id() );
$current_set_as_entire_site_post_id = static::get_set_as_entire_site_post_id( $floating_element_type );
if ( $current_set_as_entire_site_post_id === $this->get_main_id() ) {
$actions['set_as_entire_site'] = sprintf(
'<a style="color:red;" href="?post=%s&action=remove_from_entire_site&_wpnonce=%s">%s</a>',
$this->get_post()->ID,
wp_create_nonce( 'remove_from_entire_site_' . $this->get_post()->ID ),
esc_html__( 'Remove From Entire Site', 'elementor' )
);
} else {
$actions['set_as_entire_site'] = sprintf(
'<a href="?post=%s&action=set_as_entire_site&_wpnonce=%s">%s</a>',
$this->get_post()->ID,
wp_create_nonce( 'set_as_entire_site_' . $this->get_post()->ID ),
esc_html__( 'Set as Entire Site', 'elementor' )
);
}
return $actions;
}
public static function get_title() {
return esc_html__( 'Floating Element', 'elementor' );
}
public static function get_plural_title() {
return esc_html__( 'Floating Elements', 'elementor' );
}
public static function get_create_url() {
return parent::get_create_url() . '#library';
}
public function save( $data ) {
if ( empty( $data['settings']['template'] ) ) {
$data['settings']['template'] = Page_Templates_Module::TEMPLATE_CANVAS;
}
return parent::save( $data );
}
public function admin_columns_content( $column_name ) {
if ( 'elementor_library_type' === $column_name ) {
$admin_filter_url = admin_url( Source_Local::ADMIN_MENU_SLUG . '&elementor_library_type=' . $this->get_name() );
$meta = static::get_floating_element_type( $this->get_main_id() );
printf( '<a href="%s">%s</a>', $admin_filter_url, Module::get_floating_elements_types()[ $meta ] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
}
public function get_edit_url() {
return add_query_arg(
[
'post' => $this->get_main_id(),
'action' => 'elementor',
'floating_element' => get_post_meta(
$this->get_main_id(),
Module::FLOATING_ELEMENTS_TYPE_META_KEY,
true
),
],
admin_url( 'post.php' )
);
}
protected function get_remote_library_config() {
$config = [
'type' => 'floating_button',
'default_route' => 'templates/floating-buttons',
'autoImportSettings' => true,
];
return array_replace_recursive( parent::get_remote_library_config(), $config );
}
}

View File

@@ -0,0 +1,572 @@
<?php
namespace Elementor\Modules\FloatingButtons;
use Elementor\Controls_Manager;
use Elementor\Core\Admin\Menu\Admin_Menu_Manager;
use Elementor\Core\Base\Document;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Documents_Manager;
use Elementor\Core\Experiments\Manager;
use Elementor\Modules\FloatingButtons\Base\Widget_Floating_Bars_Base;
use Elementor\Modules\FloatingButtons\AdminMenuItems\Floating_Buttons_Empty_View_Menu_Item;
use Elementor\Modules\FloatingButtons\AdminMenuItems\Floating_Buttons_Menu_Item;
use Elementor\Modules\FloatingButtons\Base\Widget_Contact_Button_Base;
use Elementor\Modules\FloatingButtons\Control\Hover_Animation_Floating_Buttons;
use Elementor\Modules\FloatingButtons\Documents\Floating_Buttons;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Utils as ElementorUtils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Module extends BaseModule {
const EXPERIMENT_NAME = 'floating-buttons';
const FLOATING_ELEMENTS_TYPE_META_KEY = '_elementor_floating_elements_type';
const ROUTER_OPTION_KEY = 'elementor_floating_buttons_router_version';
const META_CLICK_TRACKING = '_elementor_click_tracking';
const CLICK_TRACKING_NONCE = 'elementor-conversion-center-click';
const FLOATING_BUTTONS_DOCUMENT_TYPE = 'floating-buttons';
const CPT_FLOATING_BUTTONS = 'e-floating-buttons';
const ADMIN_PAGE_SLUG_CONTACT = 'edit.php?post_type=e-floating-buttons';
private $has_contact_pages = null;
private $trashed_contact_pages;
public static function is_active(): bool {
return Plugin::$instance->experiments->is_feature_active( 'container' );
}
public static function get_floating_elements_types() {
return [
'floating-buttons' => esc_html__( 'Floating Buttons', 'elementor' ),
'floating-bars' => esc_html__( 'Floating Bars', 'elementor' ),
];
}
// TODO: This is a hidden experiment which needs to remain enabled like this until 3.26 for pro compatibility.
public static function get_experimental_data() {
return [
'name' => self::EXPERIMENT_NAME,
'title' => esc_html__( 'Floating Buttons', 'elementor' ),
'hidden' => true,
'default' => Manager::STATE_ACTIVE,
'release_status' => Manager::RELEASE_STATUS_STABLE,
'mutable' => false,
];
}
public function get_name(): string {
return static::EXPERIMENT_NAME;
}
public function get_widgets(): array {
return [
'Contact_Buttons',
'Floating_Bars_Var_1',
];
}
private function register_admin_menu_legacy( Admin_Menu_Manager $admin_menu ) {
$menu_args = $this->get_contact_menu_args();
$function = $menu_args['function'];
if ( is_callable( $function ) ) {
$admin_menu->register( $menu_args['menu_slug'], new Floating_Buttons_Empty_View_Menu_Item( $function ) );
} else {
$admin_menu->register( $menu_args['menu_slug'], new Floating_Buttons_Menu_Item() );
};
}
public function __construct() {
parent::__construct();
if ( Floating_Buttons::is_creating_floating_buttons_page() || Floating_Buttons::is_editing_existing_floating_buttons_page() ) {
Controls_Manager::add_tab(
Widget_Contact_Button_Base::TAB_ADVANCED,
esc_html__( 'Advanced', 'elementor' )
);
Controls_Manager::add_tab(
Widget_Floating_Bars_Base::TAB_ADVANCED,
esc_html__( 'Advanced', 'elementor' )
);
}
$this->register_contact_pages_cpt();
if ( ! ElementorUtils::has_pro() ) {
add_action( 'elementor/documents/register', function ( Documents_Manager $documents_manager ) {
$documents_manager->register_document_type(
static::FLOATING_BUTTONS_DOCUMENT_TYPE,
Floating_Buttons::get_class_full_name()
);
} );
}
add_action( 'current_screen', function() {
$screen = get_current_screen();
if ( $screen && 'edit-e-floating-buttons' === $screen->id ) {
$this->flush_permalinks_on_elementor_version_change();
}
});
add_action( 'wp_ajax_elementor_send_clicks', [ $this, 'handle_click_tracking' ] );
add_action( 'wp_ajax_nopriv_elementor_send_clicks', [ $this, 'handle_click_tracking' ] );
add_action( 'elementor/frontend/after_register_styles', [ $this, 'register_styles' ] );
add_action( 'elementor/controls/register', function ( Controls_Manager $controls_manager ) {
$controls_manager->register( new Hover_Animation_Floating_Buttons() );
});
add_filter( 'elementor/widget/common/register_css_attributes_control', function ( $common_controls ) {
if ( Floating_Buttons::is_creating_floating_buttons_page() || Floating_Buttons::is_editing_existing_floating_buttons_page() ) {
return false;
}
return $common_controls;
} );
add_filter( 'elementor/settings/controls/checkbox_list_cpt/post_type_objects', function ( $post_types ) {
unset( $post_types[ static::CPT_FLOATING_BUTTONS ] );
return $post_types;
} );
add_filter(
'elementor/template_library/sources/local/is_valid_template_type',
function ( $is_valid_template_type, $cpt ) {
if ( in_array( static::CPT_FLOATING_BUTTONS, $cpt, true ) ) {
return true;
}
return $is_valid_template_type;
},
10,
2
);
if ( ! ElementorUtils::has_pro() ) {
add_action( 'wp_footer', function () {
$this->render_floating_buttons();
} );
}
add_action( 'elementor/admin-top-bar/is-active', function ( $is_top_bar_active, $current_screen ) {
if ( strpos( $current_screen->id ?? '', static::CPT_FLOATING_BUTTONS ) !== false ) {
return true;
}
return $is_top_bar_active;
}, 10, 2 );
add_action( 'elementor/admin/menu/register', function( Admin_Menu_Manager $admin_menu ) {
$this->register_admin_menu_legacy( $admin_menu );
}, Source_Local::ADMIN_MENU_PRIORITY + 20 );
add_action( 'elementor/admin/localize_settings', function ( array $settings ) {
return $this->admin_localize_settings( $settings );
} );
add_action( 'elementor/editor/localize_settings', function ( $data ) {
return $this->editor_localize_settings( $data );
} );
add_filter( 'elementor/template_library/sources/local/register_taxonomy_cpts', function ( array $cpts ) {
$cpts[] = static::CPT_FLOATING_BUTTONS;
return $cpts;
} );
add_action( 'admin_init', function () {
$action = filter_input( INPUT_GET, 'action' );
$menu_args = $this->get_contact_menu_args();
switch ( $action ) {
case 'remove_from_entire_site':
$post = filter_input( INPUT_GET, 'post', FILTER_VALIDATE_INT );
check_admin_referer( 'remove_from_entire_site_' . $post );
delete_post_meta( $post, '_elementor_conditions' );
wp_redirect( $menu_args['menu_slug'] );
exit;
case 'set_as_entire_site':
$post = filter_input( INPUT_GET, 'post', FILTER_VALIDATE_INT );
check_admin_referer( 'set_as_entire_site_' . $post );
$posts = get_posts( [
'post_type' => static::CPT_FLOATING_BUTTONS,
'posts_per_page' => -1,
'post_status' => 'publish',
'fields' => 'ids',
'no_found_rows' => true,
'update_post_term_cache' => false,
'update_post_meta_cache' => false,
'meta_query' => Floating_Buttons::get_meta_query_for_floating_buttons(
Floating_Buttons::get_floating_element_type( $post )
),
] );
foreach ( $posts as $post_id ) {
delete_post_meta( $post_id, '_elementor_conditions' );
}
update_post_meta( $post, '_elementor_conditions', [ 'include/general' ] );
wp_redirect( $menu_args['menu_slug'] );
exit;
default:
break;
}
} );
add_action( 'manage_' . static::CPT_FLOATING_BUTTONS . '_posts_columns', function( $posts_columns ) {
$source_local = Plugin::$instance->templates_manager->get_source( 'local' );
unset( $posts_columns['date'] );
unset( $posts_columns['comments'] );
$posts_columns['click_tracking'] = esc_html__( 'Click Tracking', 'elementor' );
if ( ! ElementorUtils::has_pro() ) {
$posts_columns['instances'] = esc_html__( 'Instances', 'elementor' );
}
return $source_local->admin_columns_headers( $posts_columns );
} );
add_action(
'manage_' . static::CPT_FLOATING_BUTTONS . '_posts_custom_column',
[ $this, 'set_admin_columns_content' ],
10,
2
);
add_action( 'admin_bar_menu', function ( $admin_bar ) {
$this->override_admin_bar_add_contact( $admin_bar );
}, 100 );
}
public function is_preview_for_document( $post_id ) {
$preview_id = ElementorUtils::get_super_global_value( $_GET, 'preview_id' );
$preview = ElementorUtils::get_super_global_value( $_GET, 'preview' );
return 'true' === $preview && (int) $post_id === (int) $preview_id;
}
public function handle_click_tracking() {
$data = filter_input_array( INPUT_POST, [
'clicks' => [
'filter' => FILTER_VALIDATE_INT,
'flags' => FILTER_REQUIRE_ARRAY,
],
'_nonce' => FILTER_UNSAFE_RAW,
] );
if ( ! wp_verify_nonce( $data['_nonce'], static::CLICK_TRACKING_NONCE ) ) {
wp_send_json_error( [ 'message' => 'Invalid nonce' ] );
}
if ( ! check_ajax_referer( static::CLICK_TRACKING_NONCE, '_nonce', false ) ) {
wp_send_json_error( [ 'message' => 'Invalid referrer' ] );
}
$posts_to_update = [];
foreach ( $data['clicks'] as $post_id ) {
if ( ! isset( $posts_to_update[ $post_id ] ) ) {
$starting_clicks = (int) get_post_meta( $post_id, static::META_CLICK_TRACKING, true );
$posts_to_update[ $post_id ] = $starting_clicks ? $starting_clicks : 0;
}
$posts_to_update[ $post_id ] ++;
}
foreach ( $posts_to_update as $post_id => $clicks ) {
update_post_meta( $post_id, static::META_CLICK_TRACKING, $clicks );
}
wp_send_json_success();
}
public function set_admin_columns_content( $column_name, $post_id ) {
$document = Plugin::$instance->documents->get( $post_id );
if ( method_exists( $document, 'admin_columns_content' ) ) {
$document->admin_columns_content( $column_name );
}
switch ( $column_name ) {
case 'click_tracking':
$click_tracking = get_post_meta( $post_id, static::META_CLICK_TRACKING, true );
echo esc_html( $click_tracking );
break;
case 'instances':
if ( ElementorUtils::has_pro() ) {
break;
}
$instances = get_post_meta( $post_id, '_elementor_conditions', true );
if ( $instances ) {
echo esc_html__( 'Entire Site', 'elementor' );
}
break;
default:
break;
}
}
public function flush_permalinks_on_elementor_version_change() {
if ( get_option( static::ROUTER_OPTION_KEY ) !== ELEMENTOR_VERSION ) {
flush_rewrite_rules();
update_option( static::ROUTER_OPTION_KEY, ELEMENTOR_VERSION );
}
}
private function get_trashed_contact_posts(): array {
if ( $this->trashed_contact_pages ) {
return $this->trashed_contact_pages;
}
$this->trashed_contact_pages = $this->get_trashed_posts(
static::CPT_FLOATING_BUTTONS,
static::FLOATING_BUTTONS_DOCUMENT_TYPE
);
return $this->trashed_contact_pages;
}
private function get_trashed_posts( string $cpt, string $document_type ) {
$query = new \WP_Query( [
'no_found_rows' => true,
'post_type' => $cpt,
'post_status' => 'trash',
'posts_per_page' => 1,
'meta_key' => '_elementor_template_type',
'meta_value' => $document_type,
] );
return $query->posts;
}
private function get_add_new_contact_page_url() {
if ( ElementorUtils::has_pro() ) {
return Plugin::$instance->documents->get_create_new_post_url(
static::CPT_FLOATING_BUTTONS,
static::FLOATING_BUTTONS_DOCUMENT_TYPE
);
}
return Plugin::$instance->documents->get_create_new_post_url(
static::CPT_FLOATING_BUTTONS,
static::FLOATING_BUTTONS_DOCUMENT_TYPE
) . '#library';
}
public function print_empty_contact_pages_page() {
$template_sources = Plugin::$instance->templates_manager->get_registered_sources();
$source_local = $template_sources['local'];
$trashed_posts = $this->get_trashed_contact_posts();
?>
<div class="e-landing-pages-empty">
<?php
/** @var Source_Local $source_local */
$source_local->print_blank_state_template(
esc_html__( 'Floating Element', 'elementor' ),
$this->get_add_new_contact_page_url(),
nl2br( esc_html__( 'Add a Floating element so your users can easily get in touch!', 'elementor' ) )
);
if ( ! empty( $trashed_posts ) ) : ?>
<div class="e-trashed-items">
<?php
printf(
/* translators: %1$s Link open tag, %2$s: Link close tag. */
esc_html__( 'Or view %1$sTrashed Items%1$s', 'elementor' ),
'<a href="' . esc_url( admin_url( 'edit.php?post_status=trash&post_type=' . self::CPT_FLOATING_BUTTONS ) ) . '">',
'</a>'
);
?>
</div>
<?php endif; ?>
</div>
<?php
}
private function admin_localize_settings( $settings ) {
$contact_menu_slug = $this->get_contact_menu_args()['menu_slug'];
if ( static::CPT_FLOATING_BUTTONS === $contact_menu_slug ) {
$contact_menu_slug = 'admin.php?page=' . $contact_menu_slug;
}
$additional_settings = [
'urls' => [
'addNewLinkUrlContact' => $this->get_add_new_contact_page_url(),
'viewContactPageUrl' => $contact_menu_slug,
],
'contactPages' => [
'hasPages' => $this->has_contact_pages(),
],
];
return array_replace_recursive( $settings, $additional_settings );
}
private function register_contact_pages_cpt() {
$this->register_post_type(
Floating_Buttons::get_labels(),
static::CPT_FLOATING_BUTTONS
);
}
private function register_post_type( array $labels, string $cpt ) {
$args = [
'labels' => $labels,
'public' => true,
'show_in_menu' => 'edit.php?post_type=elementor_library&tabs_group=library',
'show_in_nav_menus' => false,
'capability_type' => 'page',
'taxonomies' => [ Source_Local::TAXONOMY_TYPE_SLUG ],
'supports' => [
'title',
'editor',
'comments',
'revisions',
'trackbacks',
'author',
'excerpt',
'page-attributes',
'thumbnail',
'custom-fields',
'post-formats',
'elementor',
],
];
register_post_type( $cpt, $args );
}
private function has_contact_pages(): bool {
if ( null !== $this->has_contact_pages ) {
return $this->has_contact_pages;
}
$this->has_contact_pages = $this->has_pages(
static::CPT_FLOATING_BUTTONS,
static::FLOATING_BUTTONS_DOCUMENT_TYPE
);
return $this->has_contact_pages;
}
private function has_pages( string $cpt, string $document_type ): bool {
$posts_query = new \WP_Query( [
'no_found_rows' => true,
'post_type' => $cpt,
'post_status' => 'any',
'posts_per_page' => 1,
'meta_key' => '_elementor_template_type',
'meta_value' => $document_type,
] );
return $posts_query->post_count > 0;
}
private function get_contact_menu_args(): array {
if ( $this->has_contact_pages() ) {
$menu_slug = static::ADMIN_PAGE_SLUG_CONTACT;
$function = null;
} else {
$menu_slug = static::CPT_FLOATING_BUTTONS;
$function = [ $this, 'print_empty_contact_pages_page' ];
}
return [
'menu_slug' => $menu_slug,
'function' => $function,
];
}
public function override_admin_bar_add_contact( $admin_bar ): void {
$new_contact_page_node = $admin_bar->get_node( 'new-e-floating-buttons' );
if ( $new_contact_page_node ) {
$new_contact_page_node->href = $this->get_add_new_contact_page_url();
$admin_bar->add_node( $new_contact_page_node );
}
}
private function editor_localize_settings( $data ) {
$data['admin_floating_button_admin_url'] = admin_url( $this->get_contact_menu_args()['menu_slug'] );
return $data;
}
private function render_floating_buttons(): void {
if ( Plugin::$instance->preview->is_preview_mode() ) {
$post_id = ElementorUtils::get_super_global_value( $_GET, 'elementor-preview' );
$document = Plugin::$instance->documents->get( $post_id );
if (
$document instanceof Document &&
$document->get_name() === static::FLOATING_BUTTONS_DOCUMENT_TYPE
) {
return;
}
}
$query = new \WP_Query( [
'post_type' => static::CPT_FLOATING_BUTTONS,
'posts_per_page' => - 1,
'post_status' => 'publish',
'fields' => 'ids',
'meta_key' => '_elementor_conditions',
'meta_compare' => 'EXISTS',
] );
if ( ! $query->have_posts() ) {
return;
}
foreach ( $query->posts as $post_id ) {
$conditions = get_post_meta( $post_id, '_elementor_conditions', true );
if ( ! $conditions ) {
continue;
}
if (
in_array( 'include/general', $conditions ) &&
! $this->is_preview_for_document( $post_id ) &&
get_the_ID() !== $post_id
) {
$document = Plugin::$instance->documents->get( $post_id );
$document->print_content();
}
}
}
/**
* Register styles.
*
* At build time, Elementor compiles `/modules/floating-buttons/assets/scss/frontend.scss`
* to `/assets/css/widget-floating-buttons.min.css`.
*
* @return void
*/
public function register_styles() {
wp_register_style(
'widget-floating-buttons',
$this->get_css_assets_url( 'widget-floating-buttons', null, true, true ),
[ 'elementor-icons' ],
ELEMENTOR_VERSION
);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Elementor\Modules\FloatingButtons\Widgets;
use Elementor\Modules\FloatingButtons\Base\Widget_Contact_Button_Base;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Elementor Contact Buttons widget.
*
* Elementor widget that displays contact buttons and a chat-like prompt message.
*
* @since 3.23.0
*/
class Contact_Buttons extends Widget_Contact_Button_Base {
public function get_name(): string {
return 'contact-buttons';
}
public function get_title(): string {
return esc_html__( 'Single Chat', 'elementor' );
}
public function get_style_depends(): array {
return [ 'widget-floating-buttons' ];
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Elementor\Modules\FloatingButtons\Widgets;
use Elementor\Modules\FloatingButtons\Base\Widget_Floating_Bars_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Elementor Floating Bars Var 1 widget.
*
* Elementor widget that displays a banner with icon and link
*
* @since 3.23.0
*/
class Floating_Bars_Var_1 extends Widget_Floating_Bars_Base {
public function get_name(): string {
return 'floating-bars-var-1';
}
public function get_title(): string {
return esc_html__( 'Floating Bar CTA', 'elementor' );
}
public function get_group_name(): string {
return 'floating-bars';
}
public function get_style_depends(): array {
return [ 'widget-floating-buttons' ];
}
public function render(): void {
$this->add_inline_editing_attributes( 'announcement_text', 'none' );
$this->add_inline_editing_attributes( 'cta_text', 'none' );
parent::render();
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace Elementor\Modules\GeneratorTag;
use Elementor\Plugin;
use Elementor\Settings;
use Elementor\Core\Base\Module as BaseModule;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseModule {
public function get_name() {
return 'generator-tag';
}
public function __construct() {
parent::__construct();
add_action( 'wp_head', [ $this, 'render_generator_tag' ] );
add_action( 'elementor/admin/after_create_settings/' . Settings::PAGE_ID, [ $this, 'register_admin_settings' ], 100 );
}
public function render_generator_tag() {
if ( '1' === get_option( 'elementor_meta_generator_tag' ) ) {
return;
}
$generator_content = $this->get_generator_content();
echo '<meta name="generator" content="' . esc_attr( $generator_content ) . '">' . PHP_EOL;
}
private function get_generator_content(): string {
$active_features = $this->get_active_features();
$settings = $this->get_generator_tag_settings();
$tags = [
'Elementor ' . ELEMENTOR_VERSION,
];
if ( ! empty( $active_features ) ) {
$tags[] = 'features: ' . implode( ', ', $active_features );
}
if ( ! empty( $settings ) ) {
$tags[] = 'settings: ' . implode( ', ', $settings );
}
return implode( '; ', $tags );
}
private function get_active_features(): array {
$active_features = [];
foreach ( Plugin::$instance->experiments->get_active_features() as $feature_slug => $feature ) {
if ( isset( $feature['generator_tag'] ) && $feature['generator_tag'] ) {
$active_features[] = $feature_slug;
}
}
return $active_features;
}
private function get_generator_tag_settings(): array {
return apply_filters( 'elementor/generator_tag/settings', [] );
}
public function register_admin_settings( Settings $settings ) {
$settings->add_field(
Settings::TAB_ADVANCED,
Settings::TAB_ADVANCED,
'meta_generator_tag',
[
'label' => esc_html__( 'Generator Tag', 'elementor' ),
'field_args' => [
'type' => 'select',
'std' => '',
'options' => [
'' => esc_html__( 'Enable', 'elementor' ),
'1' => esc_html__( 'Disable', 'elementor' ),
],
'desc' => esc_html__( 'A generator tag is a meta element that indicates the attributes used to create a webpage. It is used for analytical purposes.', 'elementor' ),
],
]
);
}
}

View File

@@ -0,0 +1,231 @@
<?php
namespace Elementor\Modules\Gutenberg;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Experiments\Manager as Experiments_Manager;
use Elementor\Plugin;
use Elementor\User;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseModule {
protected $is_gutenberg_editor_active = false;
/**
* @since 2.1.0
* @access public
*/
public function get_name() {
return 'gutenberg';
}
/**
* @since 2.1.0
* @access public
* @static
*/
public static function is_active() {
return function_exists( 'register_block_type' );
}
/**
* @since 2.1.0
* @access public
*/
public function register_elementor_rest_field() {
register_rest_field( get_post_types( '', 'names' ),
'gutenberg_elementor_mode', [
'update_callback' => function( $request_value, $object ) {
if ( ! User::is_current_user_can_edit( $object->ID ) ) {
return false;
}
$document = Plugin::$instance->documents->get( $object->ID );
if ( ! $document ) {
return false;
}
$document->set_is_built_with_elementor( false );
return true;
},
]
);
}
/**
* @since 2.1.0
* @access public
*/
public function enqueue_assets() {
$document = Plugin::$instance->documents->get( get_the_ID() );
if ( ! $document || ! $document->is_editable_by_current_user() ) {
return;
}
$this->is_gutenberg_editor_active = true;
$suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
wp_enqueue_script( 'elementor-gutenberg', ELEMENTOR_ASSETS_URL . 'js/gutenberg' . $suffix . '.js', [ 'jquery' ], ELEMENTOR_VERSION, true );
$elementor_settings = [
'isElementorMode' => $document->is_built_with_elementor(),
'editLink' => $document->get_edit_url(),
];
Utils::print_js_config( 'elementor-gutenberg', 'ElementorGutenbergSettings', $elementor_settings );
}
/**
* @since 2.1.0
* @access public
*/
public function print_admin_js_template() {
if ( ! $this->is_gutenberg_editor_active ) {
return;
}
?>
<script id="elementor-gutenberg-button-switch-mode" type="text/html">
<div id="elementor-switch-mode">
<button id="elementor-switch-mode-button" type="button" class="button button-primary button-large">
<span class="elementor-switch-mode-on"><?php echo esc_html__( '&#8592; Back to WordPress Editor', 'elementor' ); ?></span>
<span class="elementor-switch-mode-off">
<i class="eicon-elementor-square" aria-hidden="true"></i>
<?php echo esc_html__( 'Edit with Elementor', 'elementor' ); ?>
</span>
</button>
</div>
</script>
<script id="elementor-gutenberg-panel" type="text/html">
<div id="elementor-editor">
<div id="elementor-go-to-edit-page-link">
<button id="elementor-editor-button" class="button button-primary button-hero">
<i class="eicon-elementor-square" aria-hidden="true"></i>
<?php echo esc_html__( 'Edit with Elementor', 'elementor' ); ?>
</button>
<div class="elementor-loader-wrapper">
<div class="elementor-loader">
<div class="elementor-loader-boxes">
<div class="elementor-loader-box"></div>
<div class="elementor-loader-box"></div>
<div class="elementor-loader-box"></div>
<div class="elementor-loader-box"></div>
</div>
</div>
<div class="elementor-loading-title"><?php echo esc_html__( 'Loading', 'elementor' ); ?></div>
</div>
</div>
</div>
</script>
<script id="elementor-gutenberg-button-tmpl" type="text/html">
<div id="elementor-edit-button-gutenberg">
<button id="elementor-edit-mode-button" type="button" class="button button-primary button-large">
<span class="elementor-edit-mode-gutenberg">
<i class="eicon-elementor-square" aria-hidden="true"></i>
<?php echo esc_html__( 'Edit with Elementor', 'elementor' ); ?>
</span>
</button>
</div>
</script>
<?php
}
/**
* @since 2.1.0
* @access public
*/
public function __construct() {
add_action( 'rest_api_init', [ $this, 'register_elementor_rest_field' ] );
add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_assets' ] );
add_action( 'admin_footer', [ $this, 'print_admin_js_template' ] );
add_action( 'wp_enqueue_scripts', [ $this, 'dequeue_assets' ], 999 );
}
public function dequeue_assets() {
if ( ! static::is_optimized_gutenberg_loading_enabled() ) {
return;
}
if ( ! static::should_dequeue_gutenberg_assets() ) {
return;
}
wp_dequeue_style( 'wp-block-library' );
wp_dequeue_style( 'wp-block-library-theme' );
wp_dequeue_style( 'wc-block-style' );
wp_dequeue_style( 'wc-blocks-style' );
}
/**
* Check whether the "Optimized Gutenberg Loading" settings is enabled.
*
* The 'elementor_optimized_gutenberg_loading' option can be enabled/disabled from the Elementor settings.
* For BC, when the option has not been saved in the database, the default '1' value is returned.
*
* @since 3.21.0
* @access private
*/
private static function is_optimized_gutenberg_loading_enabled() : bool {
return (bool) get_option( 'elementor_optimized_gutenberg_loading', '1' );
}
private static function should_dequeue_gutenberg_assets() : bool {
$post = get_post();
if ( empty( $post->ID ) ) {
return false;
}
if ( ! static::is_built_with_elementor( $post ) ) {
return false;
}
if ( static::is_gutenberg_in_post( $post ) ) {
return false;
}
return true;
}
private static function is_built_with_elementor( $post ) : bool {
$document = Plugin::$instance->documents->get( $post->ID );
if ( ! $document || ! $document->is_built_with_elementor() ) {
return false;
}
return true;
}
private static function is_gutenberg_in_post( $post ) : bool {
if ( has_blocks( $post ) ) {
return true;
}
if ( static::current_theme_is_fse_theme() ) {
return true;
}
return false;
}
private static function current_theme_is_fse_theme() : bool {
if ( function_exists( 'wp_is_block_theme' ) ) {
return (bool) wp_is_block_theme();
}
if ( function_exists( 'gutenberg_is_fse_theme' ) ) {
return (bool) gutenberg_is_fse_theme();
}
return false;
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Elementor\Modules\History;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor history module.
*
* Elementor history module handler class is responsible for registering and
* managing Elementor history modules.
*
* @since 1.7.0
*/
class Module extends BaseModule {
/**
* Get module name.
*
* Retrieve the history module name.
*
* @since 1.7.0
* @access public
*
* @return string Module name.
*/
public function get_name() {
return 'history';
}
/**
* @since 2.3.0
* @access public
*/
public function add_templates() {
Plugin::$instance->common->add_template( __DIR__ . '/views/history-panel-template.php' );
Plugin::$instance->common->add_template( __DIR__ . '/views/revisions-panel-template.php' );
}
/**
* History module constructor.
*
* Initializing Elementor history module.
*
* @since 1.7.0
* @access public
*/
public function __construct() {
add_action( 'elementor/editor/init', [ $this, 'add_templates' ] );
}
}

View File

@@ -0,0 +1,416 @@
<?php
namespace Elementor\Modules\History;
use Elementor\Core\Base\Document;
use Elementor\Core\Common\Modules\Ajax\Module as Ajax;
use Elementor\Core\Files\CSS\Post as Post_CSS;
use Elementor\Plugin;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor history revisions manager.
*
* Elementor history revisions manager handler class is responsible for
* registering and managing Elementor revisions manager.
*
* @since 1.7.0
*/
class Revisions_Manager {
/**
* Maximum number of revisions to display.
*/
const MAX_REVISIONS_TO_DISPLAY = 50;
/**
* Authors list.
*
* Holds all the authors.
*
* @access private
*
* @var array
*/
private static $authors = [];
/**
* History revisions manager constructor.
*
* Initializing Elementor history revisions manager.
*
* @since 1.7.0
* @access public
*/
public function __construct() {
self::register_actions();
}
/**
* @since 1.7.0
* @access public
* @static
*/
public static function handle_revision() {
add_filter( 'wp_save_post_revision_check_for_changes', '__return_false' );
}
/**
* @since 2.0.0
* @access public
* @static
*
* @param $post_content
* @param $post_id
*
* @return string
*/
public static function avoid_delete_auto_save( $post_content, $post_id ) {
// Add a temporary string in order the $post will not be equal to the $autosave
// in edit-form-advanced.php:210
$document = Plugin::$instance->documents->get( $post_id );
if ( $document && $document->is_built_with_elementor() ) {
$post_content .= '<!-- Created with Elementor -->';
}
return $post_content;
}
/**
* @since 2.0.0
* @access public
* @static
*/
public static function remove_temp_post_content() {
global $post;
$document = Plugin::$instance->documents->get( $post->ID );
if ( ! $document || ! $document->is_built_with_elementor() ) {
return;
}
$post->post_content = str_replace( '<!-- Created with Elementor -->', '', $post->post_content );
}
/**
* @since 1.7.0
* @access public
* @static
*
* @param int $post_id
* @param array $query_args
* @param bool $parse_result
*
* @return array
*/
public static function get_revisions( $post_id = 0, $query_args = [], $parse_result = true ) {
$post = get_post( $post_id );
if ( ! $post || empty( $post->ID ) ) {
return [];
}
$revisions = [];
$default_query_args = [
'posts_per_page' => self::MAX_REVISIONS_TO_DISPLAY,
'meta_key' => '_elementor_data',
];
$query_args = array_merge( $default_query_args, $query_args );
$posts = wp_get_post_revisions( $post->ID, $query_args );
if ( ! wp_revisions_enabled( $post ) ) {
$autosave = Utils::get_post_autosave( $post->ID );
if ( $autosave ) {
if ( $parse_result ) {
array_unshift( $posts, $autosave );
} else {
array_unshift( $posts, $autosave->ID );
}
}
}
if ( $parse_result ) {
array_unshift( $posts, $post );
} else {
array_unshift( $posts, $post->ID );
return $posts;
}
$current_time = current_time( 'timestamp' );
/** @var \WP_Post $revision */
foreach ( $posts as $revision ) {
$date = date_i18n( _x( 'M j @ H:i', 'revision date format', 'elementor' ), strtotime( $revision->post_modified ) );
$human_time = human_time_diff( strtotime( $revision->post_modified ), $current_time );
if ( $revision->ID === $post->ID ) {
$type = 'current';
$type_label = esc_html__( 'Current Version', 'elementor' );
} elseif ( false !== strpos( $revision->post_name, 'autosave' ) ) {
$type = 'autosave';
$type_label = esc_html__( 'Autosave', 'elementor' );
} else {
$type = 'revision';
$type_label = esc_html__( 'Revision', 'elementor' );
}
if ( ! isset( self::$authors[ $revision->post_author ] ) ) {
self::$authors[ $revision->post_author ] = [
'avatar' => get_avatar( $revision->post_author, 22 ),
'display_name' => get_the_author_meta( 'display_name', $revision->post_author ),
];
}
$revisions[] = [
'id' => $revision->ID,
'author' => self::$authors[ $revision->post_author ]['display_name'],
'timestamp' => strtotime( $revision->post_modified ),
'date' => sprintf(
/* translators: 1: Human readable time difference, 2: Date. */
esc_html__( '%1$s ago (%2$s)', 'elementor' ),
'<time>' . $human_time . '</time>',
'<time>' . $date . '</time>'
),
'type' => $type,
'typeLabel' => $type_label,
'gravatar' => self::$authors[ $revision->post_author ]['avatar'],
];
}
return $revisions;
}
/**
* @since 1.9.2
* @access public
* @static
*/
public static function update_autosave( $autosave_data ) {
self::save_revision( $autosave_data['ID'] );
}
/**
* @since 1.7.0
* @access public
* @static
*/
public static function save_revision( $revision_id ) {
$parent_id = wp_is_post_revision( $revision_id );
if ( $parent_id ) {
Plugin::$instance->db->safe_copy_elementor_meta( $parent_id, $revision_id );
}
}
/**
* @since 1.7.0
* @access public
* @static
*/
public static function restore_revision( $parent_id, $revision_id ) {
$parent = Plugin::$instance->documents->get( $parent_id );
$revision = Plugin::$instance->documents->get( $revision_id );
if ( ! $parent || ! $revision ) {
return;
}
$is_built_with_elementor = $revision->is_built_with_elementor();
$parent->set_is_built_with_elementor( $is_built_with_elementor );
if ( ! $is_built_with_elementor ) {
return;
}
Plugin::$instance->db->copy_elementor_meta( $revision_id, $parent_id );
$post_css = Post_CSS::create( $parent_id );
$post_css->update();
}
/**
* @since 2.3.0
* @access public
* @static
*
* @param $data
*
* @return array
* @throws \Exception
*/
public static function ajax_get_revision_data( array $data ) {
if ( ! isset( $data['id'] ) ) {
throw new \Exception( 'You must set the revision ID.' );
}
$revision = Plugin::$instance->documents->get_with_permissions( $data['id'] );
return [
'settings' => $revision->get_settings(),
'elements' => $revision->get_elements_data(),
];
}
/**
* @since 1.7.0
* @access public
* @static
*/
public static function add_revision_support_for_all_post_types() {
$post_types = get_post_types_by_support( 'elementor' );
foreach ( $post_types as $post_type ) {
add_post_type_support( $post_type, 'revisions' );
}
}
/**
* @since 2.0.0
* @access public
* @static
* @param array $return_data
* @param Document $document
*
* @return array
*/
public static function on_ajax_save_builder_data( $return_data, $document ) {
$post_id = $document->get_main_id();
$latest_revisions = self::get_revisions(
$post_id, [
'posts_per_page' => 1,
]
);
$all_revision_ids = self::get_revisions(
$post_id, [
'fields' => 'ids',
], false
);
// Send revisions data only if has revisions.
if ( ! empty( $latest_revisions ) ) {
$current_revision_id = self::current_revision_id( $post_id );
$return_data = array_replace_recursive( $return_data, [
'config' => [
'document' => [
'revisions' => [
'current_id' => $current_revision_id,
],
],
],
'latest_revisions' => $latest_revisions,
'revisions_ids' => $all_revision_ids,
] );
}
return $return_data;
}
/**
* @since 1.7.0
* @access public
* @static
*/
public static function db_before_save( $status, $has_changes ) {
if ( $has_changes ) {
self::handle_revision();
}
}
public static function document_config( $settings, $post_id ) {
$settings['revisions'] = [
'enabled' => ( $post_id && wp_revisions_enabled( get_post( $post_id ) ) ),
'current_id' => self::current_revision_id( $post_id ),
];
return $settings;
}
/**
* Localize settings.
*
* Add new localized settings for the revisions manager.
*
* Fired by `elementor/editor/editor_settings` filter.
*
* @since 1.7.0
* @deprecated 3.1.0
* @access public
* @static
*/
public static function editor_settings() {
Plugin::$instance->modules_manager->get_modules( 'dev-tools' )->deprecation->deprecated_function( __METHOD__, '3.1.0' );
return [];
}
/**
* @throws \Exception
*/
public static function ajax_get_revisions( $data ) {
Plugin::$instance->documents->check_permissions( $data['editor_post_id'] );
return self::get_revisions();
}
/**
* @since 2.3.0
* @access public
* @static
*/
public static function register_ajax_actions( Ajax $ajax ) {
$ajax->register_ajax_action( 'get_revisions', [ __CLASS__, 'ajax_get_revisions' ] );
$ajax->register_ajax_action( 'get_revision_data', [ __CLASS__, 'ajax_get_revision_data' ] );
}
/**
* @since 1.7.0
* @access private
* @static
*/
private static function register_actions() {
add_action( 'wp_restore_post_revision', [ __CLASS__, 'restore_revision' ], 10, 2 );
add_action( 'init', [ __CLASS__, 'add_revision_support_for_all_post_types' ], 9999 );
add_filter( 'elementor/document/config', [ __CLASS__, 'document_config' ], 10, 2 );
add_action( 'elementor/db/before_save', [ __CLASS__, 'db_before_save' ], 10, 2 );
add_action( '_wp_put_post_revision', [ __CLASS__, 'save_revision' ] );
add_action( 'wp_creating_autosave', [ __CLASS__, 'update_autosave' ] );
add_action( 'elementor/ajax/register_actions', [ __CLASS__, 'register_ajax_actions' ] );
// Hack to avoid delete the auto-save revision in WP editor.
add_filter( 'edit_post_content', [ __CLASS__, 'avoid_delete_auto_save' ], 10, 2 );
add_action( 'edit_form_after_title', [ __CLASS__, 'remove_temp_post_content' ] );
if ( wp_doing_ajax() ) {
add_filter( 'elementor/documents/ajax_save/return_data', [ __CLASS__, 'on_ajax_save_builder_data' ], 10, 2 );
}
}
/**
* @since 1.9.0
* @access private
* @static
*/
private static function current_revision_id( $post_id ) {
$current_revision_id = $post_id;
$autosave = Utils::get_post_autosave( $post_id );
if ( is_object( $autosave ) ) {
$current_revision_id = $autosave->ID;
}
return $current_revision_id;
}
}

View File

@@ -0,0 +1,37 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<script type="text/template" id="tmpl-elementor-panel-history-page">
<div id="elementor-panel-elements-navigation" class="elementor-panel-navigation">
<button class="elementor-component-tab elementor-panel-navigation-tab" data-tab="actions"><?php echo esc_html__( 'Actions', 'elementor' ); ?></button>
<button class="elementor-component-tab elementor-panel-navigation-tab" data-tab="revisions"><?php echo esc_html__( 'Revisions', 'elementor' ); ?></button>
</div>
<div id="elementor-panel-history-content"></div>
</script>
<script type="text/template" id="tmpl-elementor-panel-history-tab">
<div id="elementor-history-list"></div>
<div class="elementor-history-revisions-message"><?php echo esc_html__( 'Switch to Revisions tab for older versions', 'elementor' ); ?></div>
</script>
<script type="text/template" id="tmpl-elementor-panel-history-no-items">
<img class="elementor-nerd-box-icon" src="<?php
// PHPCS - Safe Elementor SVG
echo ELEMENTOR_ASSETS_URL . 'images/information.svg'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" loading="lazy" alt="<?php echo esc_attr__( 'Elementor', 'elementor' ); ?>" />
<div class="elementor-nerd-box-title"><?php echo esc_html__( 'No History Yet', 'elementor' ); ?></div>
<div class="elementor-nerd-box-message"><?php echo esc_html__( 'Once you start working, you\'ll be able to redo / undo any action you make in the editor.', 'elementor' ); ?></div>
</script>
<script type="text/template" id="tmpl-elementor-panel-history-item">
<div class="elementor-history-item__details">
<span class="elementor-history-item__title">{{{ title }}}</span>
<span class="elementor-history-item__subtitle">{{{ subTitle }}}</span>
<span class="elementor-history-item__action">{{{ action }}}</span>
</div>
<div class="elementor-history-item__icon">
<span class="eicon" aria-hidden="true"></span>
</div>
</script>

View File

@@ -0,0 +1,74 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<script type="text/template" id="tmpl-elementor-panel-revisions">
<div class="elementor-panel-box">
<div class="elementor-panel-revisions-buttons">
<button class="elementor-button e-btn-txt e-revision-discard" disabled>
<?php echo esc_html__( 'Discard', 'elementor' ); ?>
</button>
<button class="elementor-button e-revision-save" disabled>
<?php echo esc_html__( 'Apply', 'elementor' ); ?>
</button>
</div>
</div>
<div class="elementor-panel-box">
<div id="elementor-revisions-list" class="elementor-panel-box-content"></div>
</div>
</script>
<script type="text/template" id="tmpl-elementor-panel-revisions-no-revisions">
<#
var no_revisions_1 = '<?php echo esc_html__( 'Revision history lets you save your previous versions of your work, and restore them any time.', 'elementor' ); ?>',
no_revisions_2 = '<?php echo esc_html__( 'Start designing your page and you will be able to see the entire revision history here.', 'elementor' ); ?>',
revisions_disabled_1 = '<?php echo esc_html__( 'It looks like the post revision feature is unavailable in your website.', 'elementor' ); ?>',
revisions_disabled_2 = '<?php printf(
/* translators: %1$s Link open tag, %2$s: Link close tag. */
esc_html__( 'Learn more about %1$sWordPress revisions%2$s', 'elementor' ),
'<a target="_blank" href="https://go.elementor.com/wordpress-revisions/">',
'</a>'
); ?>';
#>
<img class="elementor-nerd-box-icon" src="<?php
// PHPCS - Safe Elementor SVG
echo ELEMENTOR_ASSETS_URL . 'images/information.svg' // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" loading="lazy" alt="<?php echo esc_attr__( 'Elementor', 'elementor' ); ?>" />
<div class="elementor-nerd-box-title"><?php echo esc_html__( 'No Revisions Saved Yet', 'elementor' ); ?></div>
<div class="elementor-nerd-box-message">{{{ elementor.config.document.revisions.enabled ? no_revisions_1 : revisions_disabled_1 }}}</div>
<div class="elementor-nerd-box-message">{{{ elementor.config.document.revisions.enabled ? no_revisions_2 : revisions_disabled_2 }}}</div>
</script>
<script type="text/template" id="tmpl-elementor-panel-revisions-loading">
<i class="eicon-loading eicon-animation-spin" aria-hidden="true"></i>
</script>
<script type="text/template" id="tmpl-elementor-panel-revisions-revision-item">
<button class="elementor-revision-item__wrapper {{ type }}">
<div class="elementor-revision-item__gravatar">{{{ gravatar }}}</div>
<div class="elementor-revision-item__details">
<div class="elementor-revision-date" title="{{{ new Date( timestamp * 1000 ) }}}">{{{ date }}}</div>
<div class="elementor-revision-meta">
<span>{{{ typeLabel }}}</span>
<?php echo esc_html__( 'By', 'elementor' ); ?> {{{ author }}}
<span>(#{{{ id }}})</span>&nbsp;
</div>
</div>
<div class="elementor-revision-item__tools">
<i class="elementor-revision-item__tools-spinner eicon-loading eicon-animation-spin" aria-hidden="true"></i>
<# if ( 'current' === type ) { #>
<i class="elementor-revision-item__tools-current eicon-check" aria-hidden="true"></i>
<span class="elementor-screen-only"><?php echo esc_html__( 'Published', 'elementor' ); ?></span>
<# } #>
<!-- <# if ( 'revision' === type ) { #>-->
<!-- <i class="eicon-undo" aria-hidden="true"></i>-->
<!-- <span class="elementor-screen-only">--><?php //echo esc_html__( 'Restore', 'elementor' ); ?><!--</span>-->
<!-- <# } #>-->
</div>
</button>
</script>

View File

@@ -0,0 +1,65 @@
<?php
namespace Elementor\Modules\Home;
use Elementor\Modules\Home\Classes\Transformations_Manager;
class API {
const HOME_SCREEN_DATA_URL = 'https://assets.elementor.com/home-screen/v1/home-screen.json';
public static function get_home_screen_items( $force_request = false ): array {
$home_screen_data = self::get_transient( '_elementor_home_screen_data' );
if ( $force_request || false === $home_screen_data ) {
$home_screen_data = static::fetch_data();
static::set_transient( '_elementor_home_screen_data', $home_screen_data, '+1 hour' );
}
return self::transform_home_screen_data( $home_screen_data );
}
private static function transform_home_screen_data( $json_data ): array {
$transformers = new Transformations_Manager( $json_data );
return $transformers->run_transformations();
}
private static function fetch_data(): array {
$response = wp_remote_get( self::HOME_SCREEN_DATA_URL );
if ( is_wp_error( $response ) ) {
return [];
}
$data = json_decode( wp_remote_retrieve_body( $response ), true );
if ( empty( $data['home-screen'] ) || ! is_array( $data['home-screen'] ) ) {
return [];
}
return $data['home-screen'];
}
private static function get_transient( $cache_key ) {
$cache = get_option( $cache_key );
if ( empty( $cache['timeout'] ) ) {
return false;
}
if ( current_time( 'timestamp' ) > $cache['timeout'] ) {
return false;
}
return json_decode( $cache['value'], true );
}
private static function set_transient( $cache_key, $value, $expiration = '+12 hours' ): bool {
$data = [
'timeout' => strtotime( $expiration, current_time( 'timestamp' ) ),
'value' => json_encode( $value ),
];
return update_option( $cache_key, $data, false );
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Elementor\Modules\Home\Classes;
use Elementor\Core\Isolation\Wordpress_Adapter;
use Elementor\Core\Isolation\Plugin_Status_Adapter;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Transformations_Manager {
private static $cached_data = [];
private const TRANSFORMATIONS = [
'Create_New_Page_Url',
'Filter_Plugins',
'Filter_Get_Started_By_License',
'Filter_Sidebar_Upgrade_By_License',
'Filter_Condition_Introduction_Meta',
'Create_Site_Settings_Url',
'Filter_Top_Section_By_License',
];
protected array $home_screen_data;
protected Wordpress_Adapter $wordpress_adapter;
protected Plugin_Status_Adapter $plugin_status_adapter;
protected array $transformation_classes = [];
public function __construct( $home_screen_data ) {
$this->home_screen_data = $home_screen_data;
$this->wordpress_adapter = new Wordpress_Adapter();
$this->plugin_status_adapter = new Plugin_Status_Adapter( $this->wordpress_adapter );
$this->transformation_classes = $this->get_transformation_classes();
}
public function run_transformations(): array {
if ( ! empty( self::$cached_data ) ) {
return self::$cached_data;
}
$transformations = self::TRANSFORMATIONS;
foreach ( $transformations as $transformation_id ) {
$this->home_screen_data = $this->transformation_classes[ $transformation_id ]->transform( $this->home_screen_data );
}
self::$cached_data = $this->home_screen_data;
return $this->home_screen_data;
}
private function get_transformation_classes(): array {
$classes = [];
$transformations = self::TRANSFORMATIONS;
$arguments = [
'wordpress_adapter' => $this->wordpress_adapter,
'plugin_status_adapter' => $this->plugin_status_adapter,
];
foreach ( $transformations as $transformation_id ) {
$class_name = '\\Elementor\\Modules\\Home\\Transformations\\' . $transformation_id;
$classes[ $transformation_id ] = new $class_name( $arguments );
}
return $classes;
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace Elementor\Modules\Home;
use Elementor\Core\Admin\Menu\Admin_Menu_Manager;
use Elementor\Core\Base\App as BaseApp;
use Elementor\Core\Experiments\Manager as Experiments_Manager;
use Elementor\Settings;
use Elementor\Plugin;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseApp {
const PAGE_ID = 'home_screen';
public function get_name(): string {
return 'home';
}
public function __construct() {
parent::__construct();
$this->register_layout_experiment();
if ( ! $this->is_experiment_active() ) {
return;
}
add_action( 'elementor/admin/menu/after_register', function ( Admin_Menu_Manager $admin_menu, array $hooks ) {
$hook_suffix = 'toplevel_page_elementor';
add_action( "admin_print_scripts-{$hook_suffix}", [ $this, 'enqueue_home_screen_scripts' ] );
}, 10, 2 );
add_filter( 'elementor/document/urls/edit', [ $this, 'add_active_document_to_edit_link' ] );
}
public function enqueue_home_screen_scripts(): void {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$min_suffix = Utils::is_script_debug() ? '' : '.min';
wp_enqueue_script(
'e-home-screen',
ELEMENTOR_ASSETS_URL . 'js/e-home-screen' . $min_suffix . '.js',
[
'react',
'react-dom',
'elementor-common',
'elementor-v2-ui',
],
ELEMENTOR_VERSION,
true
);
wp_set_script_translations( 'e-home-screen', 'elementor' );
wp_localize_script(
'e-home-screen',
'elementorHomeScreenData',
$this->get_app_js_config()
);
}
public function is_experiment_active(): bool {
return Plugin::$instance->experiments->is_feature_active( self::PAGE_ID );
}
public function add_active_document_to_edit_link( $edit_link ) {
$active_document = Utils::get_super_global_value( $_GET, 'active-document' ) ?? null;
$active_tab = Utils::get_super_global_value( $_GET, 'active-tab' ) ?? null;
if ( $active_document ) {
$edit_link = add_query_arg( 'active-document', $active_document, $edit_link );
}
if ( $active_tab ) {
$edit_link = add_query_arg( 'active-tab', $active_tab, $edit_link );
}
return $edit_link;
}
private function register_layout_experiment(): void {
Plugin::$instance->experiments->add_feature( [
'name' => static::PAGE_ID,
'title' => esc_html__( 'Elementor Home Screen', 'elementor' ),
'description' => esc_html__( 'Default Elementor menu page.', 'elementor' ),
'hidden' => true,
'default' => Experiments_Manager::STATE_ACTIVE,
] );
}
private function get_app_js_config(): array {
return API::get_home_screen_items();
}
public static function get_elementor_settings_page_id(): string {
return Plugin::$instance->experiments->is_feature_active( self::PAGE_ID )
? 'elementor-settings'
: Settings::PAGE_ID;
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Elementor\Modules\Home\Transformations\Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
abstract class Transformations_Abstract {
protected $wordpress_adapter;
protected $plugin_status_adapter;
public function __construct( $args ) {
$this->wordpress_adapter = $args['wordpress_adapter'] ?? null;
$this->plugin_status_adapter = $args['plugin_status_adapter'] ?? null;
}
abstract public function transform( array $home_screen_data ): array;
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Elementor\Modules\Home\Transformations;
use Elementor\Modules\Home\Transformations\Base\Transformations_Abstract;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Create_New_Page_Url extends Transformations_Abstract {
public function transform( array $home_screen_data ): array {
$home_screen_data['create_new_page_url'] = Plugin::$instance->documents->get_create_new_post_url( 'page' );
return $home_screen_data;
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace Elementor\Modules\Home\Transformations;
use Elementor\Core\Base\Document;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Create_Site_Settings_Url extends Base\Transformations_Abstract {
const URL_TYPE = 'site_settings';
const SITE_SETTINGS_ITEMS = [ 'Site Settings', 'Site Logo', 'Global Colors', 'Global Fonts' ];
public function transform( array $home_screen_data ): array {
if ( empty( $home_screen_data['get_started'] ) ) {
return $home_screen_data;
}
$site_settings_url_config = $this->get_site_settings_url_config();
$home_screen_data['get_started']['repeater'] = array_map( function( $repeater_item ) use ( $site_settings_url_config ) {
if ( ! in_array( $repeater_item['title'], static::SITE_SETTINGS_ITEMS, true ) ) {
return $repeater_item;
}
if ( ! empty( $repeater_item['tab_id'] ) ) {
$site_settings_url_config['url'] = add_query_arg( [ 'active-tab' => $repeater_item['tab_id'] ], $site_settings_url_config['url'] );
}
return array_merge( $repeater_item, $site_settings_url_config );
}, $home_screen_data['get_started']['repeater'] );
return $home_screen_data;
}
private function get_site_settings_url_config(): array {
$existing_elementor_page = $this->get_elementor_page();
$site_settings_url = ! empty( $existing_elementor_page )
? $this->get_elementor_edit_url( $existing_elementor_page->ID )
: $this->get_elementor_create_new_page_url();
return [
'new_page' => empty( $existing_elementor_page ),
'url' => $site_settings_url,
'type' => static::URL_TYPE,
];
}
private function get_elementor_create_new_page_url(): string {
$active_kit_id = Plugin::$instance->kits_manager->get_active_id();
if ( empty( $active_kit_id ) ) {
return Plugin::$instance->documents->get_create_new_post_url( 'page' );
}
return add_query_arg( [ 'active-document' => $active_kit_id ], Plugin::$instance->documents->get_create_new_post_url( 'page' ) );
}
private function get_elementor_edit_url( int $post_id ): string {
$active_kit_id = Plugin::$instance->kits_manager->get_active_id();
$document = Plugin::$instance->documents->get( $post_id );
if ( ! $document ) {
return '';
}
return add_query_arg( [ 'active-document' => $active_kit_id ], $document->get_edit_url() );
}
private function get_elementor_page() {
$args = [
'meta_key' => Document::BUILT_WITH_ELEMENTOR_META_KEY,
'sort_order' => 'asc',
'sort_column' => 'post_date',
];
$pages = get_pages( $args );
if ( empty( $pages ) ) {
return null;
}
$show_page_on_front = 'page' === get_option( 'show_on_front' );
if ( ! $show_page_on_front ) {
return $pages[0];
}
$home_page_id = get_option( 'page_on_front' );
foreach ( $pages as $page ) {
if ( (string) $page->ID === $home_page_id ) {
return $page;
}
}
return $pages[0];
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Elementor\Modules\Home\Transformations;
use Elementor\Modules\Home\Transformations\Base\Transformations_Abstract;
use Elementor\User;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Filter_Condition_Introduction_Meta extends Transformations_Abstract {
public array $introduction_meta_data;
public function __construct( $args ) {
parent::__construct( $args );
$this->introduction_meta_data = User::get_introduction_meta() ?? [];
}
public function transform( array $home_screen_data ): array {
$introduction_meta_conditions = $this->get_introduction_meta_conditions( $home_screen_data );
$active_addons = $this->get_activated_addons( $introduction_meta_conditions );
$home_screen_data['add_ons']['repeater'] = $this->get_inactive_addons( $home_screen_data, $active_addons );
return $home_screen_data;
}
private function get_introduction_meta_conditions( $home_screen_data ): array {
$add_ons = $home_screen_data['add_ons']['repeater'];
$conditions = [];
foreach ( $add_ons as $add_on ) {
if ( array_key_exists( 'condition', $add_on ) && 'introduction_meta' === $add_on['condition']['key'] ) {
$conditions[ $add_on['title'] ] = $add_on['condition']['value'];
}
}
return $conditions;
}
private function get_activated_addons( $conditions ): array {
$active_addons = [];
foreach ( $conditions as $add_on_title => $introduction_meta_value ) {
if ( ! empty( $this->introduction_meta_data[ $introduction_meta_value ] ) ) {
$active_addons[] = $add_on_title;
}
}
return $active_addons;
}
private function get_inactive_addons( $home_screen_data, $active_addons ): array {
$add_ons = $home_screen_data['add_ons']['repeater'];
$inactive_add_ons = [];
foreach ( $add_ons as $add_on ) {
if ( ! in_array( $add_on['title'], $active_addons ) ) {
$inactive_add_ons[] = $add_on;
}
}
return $inactive_add_ons;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Elementor\Modules\Home\Transformations;
use Elementor\Modules\Home\Transformations\Base\Transformations_Abstract;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Filter_Get_Started_By_License extends Transformations_Abstract {
public bool $has_pro;
public function __construct( $args ) {
parent::__construct( $args );
$this->has_pro = Utils::has_pro();
}
private function is_valid_item( $item ) {
$has_pro_json_not_free = $this->has_pro && 'pro' === $item['license'][0];
$is_not_pro_json_not_pro = ! $this->has_pro && 'free' === $item['license'][0];
return $has_pro_json_not_free || $is_not_pro_json_not_pro;
}
public function transform( array $home_screen_data ): array {
$new_get_started = [];
foreach ( $home_screen_data['get_started'] as $index => $item ) {
if ( $this->is_valid_item( $item ) ) {
$new_get_started[] = $item;
}
}
$home_screen_data['get_started'] = reset( $new_get_started );
return $home_screen_data;
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace Elementor\Modules\Home\Transformations;
use Elementor\Modules\Home\Transformations\Base\Transformations_Abstract;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Filter_Plugins extends Transformations_Abstract {
const PLUGIN_IS_NOT_INSTALLED_FROM_WPORG = 'not-installed-wporg';
const PLUGIN_IS_NOT_INSTALLED_NOT_FROM_WPORG = 'not-installed-not-wporg';
const PLUGIN_IS_INSTALLED_NOT_ACTIVATED = 'installed-not-activated';
const PLUGIN_IS_ACTIVATED = 'activated';
public function transform( array $home_screen_data ): array {
$home_screen_data['add_ons']['repeater'] = $this->get_add_ons_installation_status( $home_screen_data['add_ons']['repeater'] );
return $home_screen_data;
}
private function is_plugin( $add_on ): bool {
return 'link' !== $add_on['type'];
}
private function get_add_ons_installation_status( array $add_ons ): array {
$transformed_add_ons = [];
foreach ( $add_ons as $add_on ) {
if ( $this->is_plugin( $add_on ) ) {
$this->handle_plugin_add_on( $add_on, $transformed_add_ons );
} else {
$transformed_add_ons[] = $add_on;
}
}
return $transformed_add_ons;
}
private function get_plugin_installation_status( $add_on ): string {
$plugin_path = $add_on['file_path'];
if ( ! $this->plugin_status_adapter->is_plugin_installed( $plugin_path ) ) {
if ( 'wporg' === $add_on['type'] ) {
return self::PLUGIN_IS_NOT_INSTALLED_FROM_WPORG;
}
return self::PLUGIN_IS_NOT_INSTALLED_NOT_FROM_WPORG;
}
if ( $this->wordpress_adapter->is_plugin_active( $plugin_path ) ) {
return self::PLUGIN_IS_ACTIVATED;
}
return self::PLUGIN_IS_INSTALLED_NOT_ACTIVATED;
}
private function handle_plugin_add_on( array $add_on, array &$transformed_add_ons ): void {
$installation_status = $this->get_plugin_installation_status( $add_on );
if ( self::PLUGIN_IS_ACTIVATED === $installation_status ) {
return;
}
switch ( $this->get_plugin_installation_status( $add_on ) ) {
case self::PLUGIN_IS_NOT_INSTALLED_NOT_FROM_WPORG:
break;
case self::PLUGIN_IS_NOT_INSTALLED_FROM_WPORG:
$installation_url = $this->plugin_status_adapter->get_install_plugin_url( $add_on['file_path'] );
$add_on['url'] = html_entity_decode( $installation_url );
$add_on['target'] = '_self';
break;
case self::PLUGIN_IS_INSTALLED_NOT_ACTIVATED:
$activation_url = $this->plugin_status_adapter->get_activate_plugin_url( $add_on['file_path'] );
$add_on['url'] = html_entity_decode( $activation_url );
$add_on['button_label'] = esc_html__( 'Activate', 'elementor' );
$add_on['target'] = '_self';
break;
}
$transformed_add_ons[] = $add_on;
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace elementor\modules\home\transformations;
use Elementor\Modules\Home\Transformations\Base\Transformations_Abstract;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Filter_Sidebar_Upgrade_By_License extends Transformations_Abstract {
public bool $has_pro;
public function __construct( $args ) {
parent::__construct( $args );
$this->has_pro = Utils::has_pro();
}
private function is_valid_item( $item ) {
$has_pro_json_not_free = $this->has_pro && 'pro' === $item['license'][0];
$is_not_pro_json_not_pro = ! $this->has_pro && 'free' === $item['license'][0];
$should_show = ! isset( $item['show'] ) || 'true' === $item['show'];
return $has_pro_json_not_free && $should_show || $is_not_pro_json_not_pro && $should_show;
}
public function transform( array $home_screen_data ): array {
$new_sidebar_upgrade = [];
foreach ( $home_screen_data['sidebar_upgrade'] as $index => $item ) {
if ( $this->is_valid_item( $item ) ) {
$new_sidebar_upgrade[] = $item;
}
}
if ( empty( $new_sidebar_upgrade ) ) {
unset( $home_screen_data['sidebar_upgrade'] );
return $home_screen_data;
}
$home_screen_data['sidebar_upgrade'] = reset( $new_sidebar_upgrade );
return $home_screen_data;
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace elementor\modules\home\transformations;
use Elementor\Modules\Home\Transformations\Base\Transformations_Abstract;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Filter_Top_Section_By_License extends Transformations_Abstract {
public bool $has_pro;
public function __construct( $args ) {
parent::__construct( $args );
$this->has_pro = Utils::has_pro();
}
private function is_valid_item( $item ) {
if ( isset( $item['license'] ) ) {
$has_pro_json_not_free = $this->has_pro && 'pro' === $item['license'][0];
$is_not_pro_json_not_pro = ! $this->has_pro && 'free' === $item['license'][0];
return $has_pro_json_not_free || $is_not_pro_json_not_pro;
}
}
public function transform( array $home_screen_data ): array {
foreach ( $home_screen_data['top_with_licences'] as $index => $item ) {
if ( $this->is_valid_item( $item ) ) {
$new_top[] = $item;
}
}
$home_screen_data['top_with_licences'] = reset( $new_top );
unset( $home_screen_data['top'] );
return $home_screen_data;
}
}

View File

@@ -0,0 +1,364 @@
<?php
namespace Elementor\Modules\ImageLoadingOptimization;
use Elementor\Core\Base\Module as BaseModule;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Module extends BaseModule {
/**
* @var int Minimum square-pixels threshold.
*/
private $min_priority_img_pixels = 50000;
/**
* @var int The number of content media elements to not lazy-load.
*/
private $omit_threshold = 3;
/**
* @var array Keep a track of images for which loading optimization strategy were computed.
*/
private static $image_visited = [];
/**
* Get Module name.
*/
public function get_name() {
return 'image-loading-optimization';
}
/**
* Constructor.
*/
public function __construct() {
if ( ! static::is_optimized_image_loading_enabled() ) {
return;
}
parent::__construct();
// Stop wp core logic.
add_action( 'init', [ $this, 'stop_core_fetchpriority_high_logic' ] );
add_filter( 'wp_lazy_loading_enabled', '__return_false' );
// Run optimization logic on header.
add_action( 'get_header', [ $this, 'set_buffer' ] );
// Ensure buffer is flushed (if any) before the content logic.
add_filter( 'the_content', [ $this, 'flush_header_buffer' ], 0 );
// Run optimization logic on content.
add_filter( 'wp_content_img_tag', [ $this, 'loading_optimization_image' ] );
}
/**
* Check whether the "Optimized Image Loading" settings is enabled.
*
* The 'optimized_image_loading' option can be enabled/disabled from the Elementor settings.
*
* @since 3.21.0
* @access private
*/
private static function is_optimized_image_loading_enabled(): bool {
return '1' === get_option( 'elementor_optimized_image_loading', '1' );
}
/**
* Stop WordPress core fetchpriority logic by setting the wp_high_priority_element_flag flag to false.
*/
public function stop_core_fetchpriority_high_logic() {
// wp_high_priority_element_flag was only introduced in 6.3.0
if ( function_exists( 'wp_high_priority_element_flag' ) ) {
wp_high_priority_element_flag( false );
}
}
/**
* Set buffer to handle header and footer content.
*/
public function set_buffer() {
ob_start( [ $this, 'handle_buffer_content' ] );
}
/**
* This function ensure that buffer if any is flushed before the content is called.
* This function behaves more like an action than a filter.
*
* @param string $content the content.
* @return string We simply return the content from parameter.
*/
public function flush_header_buffer( $content ) {
$buffer_status = ob_get_status();
if ( ! empty( $buffer_status ) &&
1 === $buffer_status['type'] &&
get_class( $this ) . '::handle_buffer_content' === $buffer_status['name'] ) {
ob_end_flush();
}
return $content;
}
/**
* Callback to handle image optimization logic on buffered content.
*
* @param string $buffer Buffered content.
* @return string Content with optimized images.
*/
public function handle_buffer_content( $buffer ) {
return $this->filter_images( $buffer );
}
/**
* Check for image in the content provided and apply optimization logic on them.
*
* @param string $content Content to be analyzed.
* @return string Content with optimized images.
*/
private function filter_images( $content ) {
return preg_replace_callback(
'/<img\s[^>]+>/',
function ( $matches ) {
return $this->loading_optimization_image( $matches[0] );
},
$content
);
}
/**
* Apply loading optimization logic on the image.
*
* @param mixed $image Original image tag.
* @return string Optimized image.
*/
public function loading_optimization_image( $image ) {
if ( isset( self::$image_visited[ $image ] ) ) {
return self::$image_visited[ $image ];
}
$optimized_image = $this->add_loading_optimization_attrs( $image );
self::$image_visited[ $image ] = $optimized_image;
return $optimized_image;
}
/**
* Adds optimization attributes to an `img` HTML tag.
*
* @param string $image The HTML `img` tag where the attribute should be added.
* @return string Converted `img` tag with optimization attributes added.
*/
private function add_loading_optimization_attrs( $image ) {
$width = preg_match( '/ width=["\']([0-9]+)["\']/', $image, $match_width ) ? (int) $match_width[1] : null;
$height = preg_match( '/ height=["\']([0-9]+)["\']/', $image, $match_height ) ? (int) $match_height[1] : null;
$loading_val = preg_match( '/ loading=["\']([A-Za-z]+)["\']/', $image, $match_loading ) ? $match_loading[1] : null;
$fetchpriority_val = preg_match( '/ fetchpriority=["\']([A-Za-z]+)["\']/', $image, $match_fetchpriority ) ? $match_fetchpriority[1] : null;
// Images should have height and dimension width for the loading optimization attributes to be added.
if ( ! str_contains( $image, ' width="' ) || ! str_contains( $image, ' height="' ) ) {
return $image;
}
$optimization_attrs = $this->get_loading_optimization_attributes(
[
'width' => $width,
'height' => $height,
'loading' => $loading_val,
'fetchpriority' => $fetchpriority_val,
]
);
if ( ! empty( $optimization_attrs['fetchpriority'] ) ) {
$image = str_replace( '<img', '<img fetchpriority="' . esc_attr( $optimization_attrs['fetchpriority'] ) . '"', $image );
}
if ( ! empty( $optimization_attrs['loading'] ) ) {
$image = str_replace( '<img', '<img loading="' . esc_attr( $optimization_attrs['loading'] ) . '"', $image );
}
return $image;
}
/**
* Return loading Loading optimization attributes for a image with give attribute.
*
* @param array $attr Existing image attributes.
* @return array Loading optimization attributes.
*/
private function get_loading_optimization_attributes( $attr ) {
$loading_attrs = [];
// For any resources, width and height must be provided, to avoid layout shifts.
if ( ! isset( $attr['width'], $attr['height'] ) ) {
return $loading_attrs;
}
/*
* The key function logic starts here.
*/
$maybe_in_viewport = null;
$increase_count = false;
$maybe_increase_count = false;
/*
* Logic to handle a `loading` attribute that is already provided.
*
* Copied from `wp_get_loading_optimization_attributes()`.
*/
if ( isset( $attr['loading'] ) ) {
/*
* Interpret "lazy" as not in viewport. Any other value can be
* interpreted as in viewport (realistically only "eager" or `false`
* to force-omit the attribute are other potential values).
*/
if ( 'lazy' === $attr['loading'] ) {
$maybe_in_viewport = false;
} else {
$maybe_in_viewport = true;
}
}
// Logic to handle a `fetchpriority` attribute that is already provided.
$has_fetchpriority_high_attr = ( isset( $attr['fetchpriority'] ) && 'high' === $attr['fetchpriority'] );
/*
* Handle cases where a `fetchpriority="high"` has already been set.
*
* Copied from `wp_get_loading_optimization_attributes()`.
*/
if ( $has_fetchpriority_high_attr ) {
/*
* If the image was already determined to not be in the viewport (e.g.
* from an already provided `loading` attribute), trigger a warning.
* Otherwise, the value can be interpreted as in viewport, since only
* the most important in-viewport image should have `fetchpriority` set
* to "high".
*/
if ( false === $maybe_in_viewport ) {
_doing_it_wrong(
__FUNCTION__,
esc_html__( 'An image should not be lazy-loaded and marked as high priority at the same time.', 'elementor' ),
''
);
/*
* Set `fetchpriority` here for backward-compatibility as we should
* not override what a developer decided, even though it seems
* incorrect.
*/
$loading_attrs['fetchpriority'] = 'high';
} else {
$maybe_in_viewport = true;
}
}
if ( null === $maybe_in_viewport && ! is_admin() ) {
$content_media_count = $this->increase_content_media_count( 0 );
$increase_count = true;
if ( $content_media_count < $this->omit_threshold ) {
$maybe_in_viewport = true;
} else {
$maybe_in_viewport = false;
}
}
if ( $maybe_in_viewport ) {
$loading_attrs = $this->maybe_add_fetchpriority_high_attr( $loading_attrs, $attr );
} else {
$loading_attrs['loading'] = 'lazy';
}
if ( $increase_count ) {
$this->increase_content_media_count();
} elseif ( $maybe_increase_count ) {
if ( $this->get_min_priority_img_pixels() <= $attr['width'] * $attr['height'] ) {
$this->increase_content_media_count();
}
}
return $loading_attrs;
}
/**
* Helper to get the minimum threshold for number of pixels an image needs to have to be considered "priority".
*
* @return int The minimum number of pixels (width * height). Default is 50000.
*/
private function get_min_priority_img_pixels() {
/**
* Filter the minimum pixel threshold used to determine if an image should have fetchpriority="high" applied.
*
* @see https://developer.wordpress.org/reference/hooks/wp_min_priority_img_pixels/
*
* @param int $pixels The minimum number of pixels (with * height).
* @return int The filtered value.
*/
return apply_filters( 'elementor/image-loading-optimization/min_priority_img_pixels', $this->min_priority_img_pixels );
}
/**
* Keeps a count of media image.
*
* @param int $amount Amount by which count must be increased.
* @return int current image count.
*/
private function increase_content_media_count( $amount = 1 ) {
static $content_media_count = 0;
$content_media_count += $amount;
return $content_media_count;
}
/**
* Determines whether to add `fetchpriority='high'` to loading attributes.
*
* @param array $loading_attrs Array of the loading optimization attributes for the element.
* @param array $attr Array of the attributes for the element.
* @return array Updated loading optimization attributes for the element.
*/
private function maybe_add_fetchpriority_high_attr( $loading_attrs, $attr ) {
if ( isset( $attr['fetchpriority'] ) ) {
if ( 'high' === $attr['fetchpriority'] ) {
$loading_attrs['fetchpriority'] = 'high';
$this->high_priority_element_flag( false );
}
return $loading_attrs;
}
// Lazy-loading and `fetchpriority="high"` are mutually exclusive.
if ( isset( $loading_attrs['loading'] ) && 'lazy' === $loading_attrs['loading'] ) {
return $loading_attrs;
}
if ( ! $this->high_priority_element_flag() ) {
return $loading_attrs;
}
if ( $this->get_min_priority_img_pixels() <= $attr['width'] * $attr['height'] ) {
$loading_attrs['fetchpriority'] = 'high';
$this->high_priority_element_flag( false );
}
return $loading_attrs;
}
/**
* Accesses a flag that indicates if an element is a possible candidate for `fetchpriority='high'`.
*
* @param bool $value Optional. Used to change the static variable. Default null.
* @return bool Returns true if high-priority element was marked already, otherwise false.
*/
private function high_priority_element_flag( $value = null ) {
static $high_priority_element = true;
if ( is_bool( $value ) ) {
$high_priority_element = $value;
}
return $high_priority_element;
}
}

View File

@@ -0,0 +1,152 @@
<?php
namespace Elementor\Modules\KitElementsDefaults\Data;
use Elementor\Core\Frontend\Performance;
use Elementor\Modules\KitElementsDefaults\Module;
use Elementor\Modules\KitElementsDefaults\Utils\Settings_Sanitizer;
use Elementor\Plugin;
use Elementor\Data\V2\Base\Exceptions\Error_404;
use Elementor\Data\V2\Base\Controller as Base_Controller;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Controller extends Base_Controller {
public function get_name() {
return 'kit-elements-defaults';
}
public function register_endpoints() {
$this->index_endpoint->register_item_route(\WP_REST_Server::EDITABLE, [
'id_arg_name' => 'type',
'id_arg_type_regex' => '[\w\-\_]+',
'type' => [
'type' => 'string',
'description' => 'The type of the element.',
'required' => true,
'validate_callback' => function( $type ) {
return $this->validate_type( $type );
},
],
'settings' => [
'description' => 'All the default values for the requested type',
'required' => true,
'type' => 'object',
'validate_callback' => function( $settings ) {
return is_array( $settings );
},
'sanitize_callback' => function( $settings, \WP_REST_Request $request ) {
Performance::set_use_style_controls( true );
$sanitizer = new Settings_Sanitizer(
Plugin::$instance->elements_manager,
array_keys( Plugin::$instance->widgets_manager->get_widget_types() )
);
$sanitized_data = $sanitizer
->for( $request->get_param( 'type' ) )
->using( $settings )
->remove_invalid_settings()
->kses_deep()
->get();
Performance::set_use_style_controls( false );
return $sanitized_data;
},
],
] );
$this->index_endpoint->register_item_route(\WP_REST_Server::DELETABLE, [
'id_arg_name' => 'type',
'id_arg_type_regex' => '[\w\-\_]+',
'type' => [
'type' => 'string',
'description' => 'The type of the element.',
'required' => true,
'validate_callback' => function( $type ) {
return $this->validate_type( $type );
},
],
] );
}
public function get_collection_params() {
return [];
}
public function get_items( $request ) {
$this->validate_kit();
$kit = Plugin::$instance->kits_manager->get_active_kit();
return (object) $kit->get_json_meta( Module::META_KEY );
}
public function update_item( $request ) {
$this->validate_kit();
$kit = Plugin::$instance->kits_manager->get_active_kit();
$data = $kit->get_json_meta( Module::META_KEY );
$data[ $request->get_param( 'type' ) ] = $request->get_param( 'settings' );
$kit->update_json_meta( Module::META_KEY, $data );
return (object) [];
}
public function delete_item( $request ) {
$this->validate_kit();
$kit = Plugin::$instance->kits_manager->get_active_kit();
$data = $kit->get_json_meta( Module::META_KEY );
unset( $data[ $request->get_param( 'type' ) ] );
$kit->update_json_meta( Module::META_KEY, $data );
return (object) [];
}
private function validate_kit() {
$kit = Plugin::$instance->kits_manager->get_active_kit();
$is_valid_kit = $kit && $kit->get_main_id();
if ( ! $is_valid_kit ) {
throw new Error_404( 'Kit doesn\'t exist.' );
}
}
private function validate_type( $param ) {
$element_types = array_keys( Plugin::$instance->elements_manager->get_element_types() );
$widget_types = array_keys( Plugin::$instance->widgets_manager->get_widget_types() );
return in_array(
$param,
array_merge( $element_types, $widget_types ),
true
);
}
public function get_items_permissions_check( $request ) {
return current_user_can( 'edit_posts' );
}
// TODO: Should be removed once the infra will support it.
public function get_item_permissions_check( $request ) {
return $this->get_items_permissions_check( $request );
}
public function update_item_permissions_check( $request ) {
return current_user_can( 'manage_options' );
}
public function delete_item_permissions_check( $request ) {
return current_user_can( 'manage_options' );
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Elementor\Modules\KitElementsDefaults\ImportExport;
use Elementor\App\Modules\ImportExport\Processes\Export;
use Elementor\App\Modules\ImportExport\Processes\Import;
use Elementor\Modules\KitElementsDefaults\ImportExport\Runners\Export as Export_Runner;
use Elementor\Modules\KitElementsDefaults\ImportExport\Runners\Import as Import_Runner;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Import_Export {
const FILE_NAME = 'kit-elements-defaults';
public function register() {
// Revert kit is working by default, using the site-settings runner.
add_action( 'elementor/import-export/export-kit', function ( Export $export ) {
$export->register( new Export_Runner() );
} );
add_action( 'elementor/import-export/import-kit', function ( Import $import ) {
$import->register( new Import_Runner() );
} );
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Elementor\Modules\KitElementsDefaults\ImportExport\Runners;
use Elementor\Modules\KitElementsDefaults\ImportExport\Import_Export;
use Elementor\Plugin;
use Elementor\Core\Utils\Collection;
use Elementor\Modules\KitElementsDefaults\Module;
use Elementor\Modules\KitElementsDefaults\Utils\Settings_Sanitizer;
use Elementor\App\Modules\ImportExport\Runners\Export\Export_Runner_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Export extends Export_Runner_Base {
public static function get_name() : string {
return 'elements-default-values';
}
public function should_export( array $data ) {
// Together with site-settings.
return (
isset( $data['include'] ) &&
in_array( 'settings', $data['include'], true )
);
}
public function export( array $data ) {
$kit = Plugin::$instance->kits_manager->get_active_kit();
if ( ! $kit ) {
return [
'manifest' => [],
'files' => [],
];
}
$default_values = $kit->get_json_meta( Module::META_KEY );
if ( ! $default_values ) {
return [
'manifest' => [],
'files' => [],
];
}
$sanitizer = new Settings_Sanitizer(
Plugin::$instance->elements_manager,
array_keys( Plugin::$instance->widgets_manager->get_widget_types() )
);
$default_values = ( new Collection( $default_values ) )
->map( function ( $settings, $type ) use ( $sanitizer, $kit ) {
return $sanitizer
->for( $type )
->using( $settings )
->remove_invalid_settings()
->kses_deep()
->prepare_for_export( $kit )
->get();
} )
->all();
return [
'files' => [
'path' => Import_Export::FILE_NAME,
'data' => $default_values,
],
];
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Elementor\Modules\KitElementsDefaults\ImportExport\Runners;
use Elementor\Modules\KitElementsDefaults\ImportExport\Import_Export;
use Elementor\Plugin;
use Elementor\Core\Utils\Collection;
use Elementor\Modules\KitElementsDefaults\Module;
use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;
use Elementor\Modules\KitElementsDefaults\Utils\Settings_Sanitizer;
use Elementor\App\Modules\ImportExport\Runners\Import\Import_Runner_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Import extends Import_Runner_Base {
public static function get_name() : string {
return 'elements-default-values';
}
public function should_import( array $data ) {
// Together with site-settings.
return (
isset( $data['include'] ) &&
in_array( 'settings', $data['include'], true ) &&
! empty( $data['site_settings']['settings'] ) &&
! empty( $data['extracted_directory_path'] )
);
}
public function import( array $data, array $imported_data ) {
$kit = Plugin::$instance->kits_manager->get_active_kit();
$file_name = Import_Export::FILE_NAME;
$default_values = ImportExportUtils::read_json_file( "{$data['extracted_directory_path']}/{$file_name}.json" );
if ( ! $kit || ! $default_values ) {
return [];
}
$element_types = array_keys( Plugin::$instance->elements_manager->get_element_types() );
$widget_types = array_keys( Plugin::$instance->widgets_manager->get_widget_types() );
$types = array_merge( $element_types, $widget_types );
$sanitizer = new Settings_Sanitizer(
Plugin::$instance->elements_manager,
$widget_types
);
$default_values = ( new Collection( $default_values ) )
->filter( function ( $settings, $type ) use ( $types ) {
return in_array( $type, $types, true );
} )
->map( function ( $settings, $type ) use ( $sanitizer, $kit ) {
return $sanitizer
->for( $type )
->using( $settings )
->remove_invalid_settings()
->kses_deep()
->prepare_for_import( $kit )
->get();
} )
->all();
$kit->update_json_meta( Module::META_KEY, $default_values );
return $default_values;
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Elementor\Modules\KitElementsDefaults;
use Elementor\Core\Experiments\Manager as Experiments_Manager;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Modules\KitElementsDefaults\Data\Controller;
use Elementor\Plugin;
use Elementor\Modules\KitElementsDefaults\ImportExport\Import_Export;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Module extends BaseModule {
const META_KEY = '_elementor_elements_default_values';
public function get_name() {
return 'kit-elements-defaults';
}
private function enqueue_scripts() {
wp_enqueue_script(
'elementor-kit-elements-defaults-editor',
$this->get_js_assets_url( 'kit-elements-defaults-editor' ),
[
'elementor-common',
'elementor-editor-modules',
'elementor-editor-document',
'wp-i18n',
],
ELEMENTOR_VERSION,
true
);
wp_set_script_translations( 'elementor-kit-elements-defaults-editor', 'elementor' );
}
public function __construct() {
parent::__construct();
add_action( 'elementor/editor/before_enqueue_scripts', function () {
$this->enqueue_scripts();
} );
Plugin::$instance->data_manager_v2->register_controller( new Controller() );
( new Usage() )->register();
if ( is_admin() ) {
( new Import_Export() )->register();
}
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Elementor\Modules\KitElementsDefaults;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Usage {
public function register() {
add_filter( 'elementor/tracker/send_tracking_data_params', function ( array $params ) {
$params['usages']['kit']['defaults'] = $this->get_usage_data();
return $params;
} );
}
private function get_usage_data() {
$elements_defaults = $this->get_elements_defaults() ?? [];
return [
'count' => count( $elements_defaults ),
'elements' => array_keys( $elements_defaults ),
];
}
private function get_elements_defaults() {
$kit = Plugin::$instance->kits_manager->get_active_kit();
return $kit->get_json_meta( Module::META_KEY );
}
}

View File

@@ -0,0 +1,280 @@
<?php
namespace Elementor\Modules\KitElementsDefaults\Utils;
use Elementor\Core\Breakpoints\Manager as Breakpoints_Manager;
use Elementor\Element_Base;
use Elementor\Elements_Manager;
use Elementor\Core\Base\Document;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Settings_Sanitizer {
const SPECIAL_SETTINGS = [
'__dynamic__',
'__globals__',
];
/**
* @var Elements_Manager
*/
private $elements_manager;
/**
* @var array
*/
private $widget_types;
/**
* @var Element_Base | null
*/
private $pending_element = null;
/**
* @var array | null
*/
private $pending_settings = null;
/**
* @param Elements_Manager $elements_manager
* @param array $widget_types
*/
public function __construct( Elements_Manager $elements_manager, array $widget_types = [] ) {
$this->elements_manager = $elements_manager;
$this->widget_types = $widget_types;
}
/**
* @param $type
*
* @return $this
*/
public function for( $type ) {
$this->pending_element = $this->create_element( $type );
return $this;
}
/**
* @param $settings
*
* @return $this
*/
public function using( $settings ) {
$this->pending_settings = $settings;
return $this;
}
/**
* @return $this
*/
public function reset() {
$this->pending_element = null;
$this->pending_settings = null;
return $this;
}
/**
* @return bool
*/
public function is_prepared() {
return $this->pending_element && is_array( $this->pending_settings );
}
/**
* @return $this
*/
public function remove_invalid_settings() {
if ( ! $this->is_prepared() ) {
return $this;
}
$valid_settings_keys = $this->get_valid_settings_keys(
$this->pending_element->get_controls()
);
$this->pending_settings = $this->filter_invalid_settings(
$this->pending_settings,
array_merge( $valid_settings_keys, self::SPECIAL_SETTINGS )
);
foreach ( self::SPECIAL_SETTINGS as $special_setting ) {
if ( ! isset( $this->pending_settings[ $special_setting ] ) ) {
continue;
}
$this->pending_settings[ $special_setting ] = $this->filter_invalid_settings(
$this->pending_settings[ $special_setting ],
$valid_settings_keys
);
}
return $this;
}
public function kses_deep() {
if ( ! $this->is_prepared() ) {
return $this;
}
$this->pending_settings = map_deep( $this->pending_settings, function( $value ) {
if ( ! is_string( $value ) ) {
return $value;
}
return wp_kses_post( $value );
} );
return $this;
}
/**
* @param Document $document
*
* @return $this
*/
public function prepare_for_export( Document $document ) {
return $this->run_import_export_sanitize_process( $document, 'on_export' );
}
/**
* @param Document $document
*
* @return $this
*/
public function prepare_for_import( Document $document ) {
return $this->run_import_export_sanitize_process( $document, 'on_import' );
}
/**
* @return array
*/
public function get() {
if ( ! $this->is_prepared() ) {
return [];
}
$settings = $this->pending_settings;
$this->reset();
return $settings;
}
/**
* @param string $type
* @param array $settings
*
* @return Element_Base|null
*/
private function create_element( $type ) {
$is_widget = in_array( $type, $this->widget_types, true );
$is_inner_section = 'inner-section' === $type;
if ( $is_inner_section ) {
return $this->elements_manager->create_element_instance( [
'elType' => 'section',
'isInner' => true,
'id' => '0',
] );
}
if ( $is_widget ) {
return $this->elements_manager->create_element_instance( [
'elType' => 'widget',
'widgetType' => $type,
'id' => '0',
] );
}
return $this->elements_manager->create_element_instance( [
'elType' => $type,
'id' => '0',
] );
}
/**
* @param Document $document
* @param $process_type
*
* @return $this
*/
private function run_import_export_sanitize_process( Document $document, $process_type ) {
if ( ! $this->is_prepared() ) {
return $this;
}
$result = $document->process_element_import_export(
$this->pending_element,
$process_type,
[ 'settings' => $this->pending_settings ]
);
if ( empty( $result['settings'] ) ) {
return $this;
}
$this->pending_settings = $result['settings'];
return $this;
}
/**
* Get all the available settings of a specific element, including responsive settings.
*
* @param array $controls
*
* @return array
*/
private function get_valid_settings_keys( $controls ) {
if ( ! $controls ) {
return [];
}
$control_keys = array_keys( $controls );
$optional_responsive_keys = [
Breakpoints_Manager::BREAKPOINT_KEY_MOBILE,
Breakpoints_Manager::BREAKPOINT_KEY_MOBILE_EXTRA,
Breakpoints_Manager::BREAKPOINT_KEY_TABLET,
Breakpoints_Manager::BREAKPOINT_KEY_TABLET_EXTRA,
Breakpoints_Manager::BREAKPOINT_KEY_LAPTOP,
Breakpoints_Manager::BREAKPOINT_KEY_WIDESCREEN,
];
$settings = [];
foreach ( $control_keys as $control_key ) {
// Add the responsive settings.
foreach ( $optional_responsive_keys as $responsive_key ) {
$settings[] = "{$control_key}_{$responsive_key}";
}
// Add the setting itself (not responsive).
$settings[] = $control_key;
}
return $settings;
}
/**
* Remove invalid settings.
*
* @param $settings
* @param $valid_settings_keys
*
* @return array
*/
private function filter_invalid_settings( $settings, $valid_settings_keys ) {
return array_filter(
$settings,
function ( $setting_key ) use ( $valid_settings_keys ) {
return in_array( $setting_key, $valid_settings_keys, true );
},
ARRAY_FILTER_USE_KEY
);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Elementor\Modules\LandingPages\AdminMenuItems;
use Elementor\Core\Admin\Menu\Interfaces\Admin_Menu_Item_With_Page;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Landing_Pages_Empty_View_Menu_Item extends Landing_Pages_Menu_Item implements Admin_Menu_Item_With_Page {
private $render_callback;
public function __construct( callable $render_callback ) {
$this->render_callback = $render_callback;
}
public function render() {
( $this->render_callback )();
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Elementor\Modules\LandingPages\AdminMenuItems;
use Elementor\Core\Admin\Menu\Interfaces\Admin_Menu_Item;
use Elementor\TemplateLibrary\Source_Local;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Landing_Pages_Menu_Item implements Admin_Menu_Item {
public function is_visible() {
return true;
}
public function get_parent_slug() {
return Source_Local::ADMIN_MENU_SLUG;
}
public function get_label() {
return esc_html__( 'Landing Pages', 'elementor' );
}
public function get_page_title() {
return esc_html__( 'Landing Pages', 'elementor' );
}
public function get_capability() {
return 'manage_options';
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace Elementor\Modules\LandingPages\Documents;
use Elementor\Core\DocumentTypes\PageBase;
use Elementor\Modules\LandingPages\Module as Landing_Pages_Module;
use Elementor\Modules\Library\Traits\Library;
use Elementor\Modules\PageTemplates\Module as Page_Templates_Module;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Landing_Page extends PageBase {
// Library Document Trait
use Library;
public static function get_properties() {
$properties = parent::get_properties();
$properties['support_kit'] = true;
$properties['show_in_library'] = true;
$properties['cpt'] = [ Landing_Pages_Module::CPT ];
return $properties;
}
public static function get_type() {
return Landing_Pages_Module::DOCUMENT_TYPE;
}
/**
* @access public
*/
public function get_name() {
return Landing_Pages_Module::DOCUMENT_TYPE;
}
/**
* @access public
* @static
*/
public static function get_title() {
return esc_html__( 'Landing Page', 'elementor' );
}
/**
* @access public
* @static
*/
public static function get_plural_title() {
return esc_html__( 'Landing Pages', 'elementor' );
}
public static function get_create_url() {
return parent::get_create_url() . '#library';
}
/**
* Save Document.
*
* Save an Elementor document.
*
* @since 3.1.0
* @access public
*
* @param $data
*
* @return bool
*/
public function save( $data ) {
// This is for the first time a Landing Page is created. It is done in order to load a new Landing Page with
// 'Canvas' as the default page template.
if ( empty( $data['settings']['template'] ) ) {
$data['settings']['template'] = Page_Templates_Module::TEMPLATE_CANVAS;
}
return parent::save( $data );
}
/**
* Admin Columns Content
*
* @since 3.1.0
*
* @param $column_name
* @access public
*/
public function admin_columns_content( $column_name ) {
if ( 'elementor_library_type' === $column_name ) {
$this->print_admin_column_type();
}
}
protected function get_remote_library_config() {
$config = [
'type' => 'lp',
'default_route' => 'templates/landing-pages',
'autoImportSettings' => true,
];
return array_replace_recursive( parent::get_remote_library_config(), $config );
}
}

View File

@@ -0,0 +1,504 @@
<?php
namespace Elementor\Modules\LandingPages;
use Elementor\Core\Admin\Menu\Admin_Menu_Manager;
use Elementor\Core\Admin\Menu\Main as MainMenu;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Documents_Manager;
use Elementor\Core\Experiments\Manager as Experiments_Manager;
use Elementor\Modules\LandingPages\Documents\Landing_Page;
use Elementor\Modules\LandingPages\AdminMenuItems\Landing_Pages_Menu_Item;
use Elementor\Modules\LandingPages\AdminMenuItems\Landing_Pages_Empty_View_Menu_Item;
use Elementor\Modules\LandingPages\Module as Landing_Pages_Module;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseModule {
const DOCUMENT_TYPE = 'landing-page';
const CPT = 'e-landing-page';
const ADMIN_PAGE_SLUG = 'edit.php?post_type=' . self::CPT;
private $has_pages = null;
private $trashed_posts;
private $new_lp_url;
private $permalink_structure;
public function get_name() {
return 'landing-pages';
}
/**
* Get Experimental Data
*
* Implementation of this method makes the module an experiment.
*
* @since 3.1.0
*
* @return array
*/
public static function get_experimental_data() {
return [
'name' => 'landing-pages',
'title' => esc_html__( 'Landing Pages', 'elementor' ),
'description' => esc_html__( 'Adds a new Elementor content type that allows creating beautiful landing pages instantly in a streamlined workflow.', 'elementor' ),
'release_status' => Experiments_Manager::RELEASE_STATUS_BETA,
'default' => Experiments_Manager::STATE_ACTIVE,
'new_site' => [
'default_inactive' => true,
'minimum_installation_version' => '3.22.0',
],
];
}
/**
* Get Trashed Landing Pages Posts
*
* Returns the posts property of a WP_Query run for Landing Pages with post_status of 'trash'.
*
* @since 3.1.0
*
* @return array trashed posts
*/
private function get_trashed_landing_page_posts() {
if ( $this->trashed_posts ) {
return $this->trashed_posts;
}
// `'posts_per_page' => 1` is because this is only used as an indicator to whether there are any trashed landing pages.
$trashed_posts_query = new \WP_Query( [
'no_found_rows' => true,
'post_type' => self::CPT,
'post_status' => 'trash',
'posts_per_page' => 1,
'meta_key' => '_elementor_template_type',
'meta_value' => self::DOCUMENT_TYPE,
] );
$this->trashed_posts = $trashed_posts_query->posts;
return $this->trashed_posts;
}
private function has_landing_pages() {
if ( null !== $this->has_pages ) {
return $this->has_pages;
}
$posts_query = new \WP_Query( [
'no_found_rows' => true,
'post_type' => self::CPT,
'post_status' => 'any',
'posts_per_page' => 1,
'meta_key' => '_elementor_template_type',
'meta_value' => self::DOCUMENT_TYPE,
] );
$this->has_pages = $posts_query->post_count > 0;
return $this->has_pages;
}
/**
* Is Elementor Landing Page.
*
* Check whether the post is an Elementor Landing Page.
*
* @since 3.1.0
* @access public
*
* @param \WP_Post $post Post Object
*
* @return bool Whether the post was built with Elementor.
*/
public function is_elementor_landing_page( $post ) {
return self::CPT === $post->post_type;
}
private function get_menu_args() {
if ( $this->has_landing_pages() ) {
$menu_slug = self::ADMIN_PAGE_SLUG;
$function = null;
} else {
$menu_slug = self::CPT;
$function = [ $this, 'print_empty_landing_pages_page' ];
}
return [
'menu_slug' => $menu_slug,
'function' => $function,
];
}
private function register_admin_menu( MainMenu $menu ) {
$landing_pages_title = esc_html__( 'Landing Pages', 'elementor' );
$menu_args = array_merge( $this->get_menu_args(), [
'page_title' => $landing_pages_title,
'menu_title' => $landing_pages_title,
'index' => 20,
] );
$menu->add_submenu( $menu_args );
}
/**
* Add Submenu Page
*
* Adds the 'Landing Pages' submenu item to the 'Templates' menu item.
*
* @since 3.1.0
*/
private function register_admin_menu_legacy( Admin_Menu_Manager $admin_menu ) {
$menu_args = $this->get_menu_args();
$slug = $menu_args['menu_slug'];
$function = $menu_args['function'];
if ( is_callable( $function ) ) {
$admin_menu->register( $slug, new Landing_Pages_Empty_View_Menu_Item( $function ) );
} else {
$admin_menu->register( $slug, new Landing_Pages_Menu_Item() );
}
}
/**
* Get 'Add New' Landing Page URL
*
* Retrieves the custom URL for the admin dashboard's 'Add New' button in the Landing Pages admin screen. This URL
* creates a new Landing Pages and directly opens the Elementor Editor with the Template Library modal open on the
* Landing Pages tab.
*
* @since 3.1.0
*
* @return string
*/
private function get_add_new_landing_page_url() {
if ( ! $this->new_lp_url ) {
$this->new_lp_url = Plugin::$instance->documents->get_create_new_post_url( self::CPT, self::DOCUMENT_TYPE ) . '#library';
}
return $this->new_lp_url;
}
/**
* Get Empty Landing Pages Page
*
* Prints the HTML content of the page that is displayed when there are no existing landing pages in the DB.
* Added as the callback to add_submenu_page.
*
* @since 3.1.0
*/
public function print_empty_landing_pages_page() {
$template_sources = Plugin::$instance->templates_manager->get_registered_sources();
$source_local = $template_sources['local'];
$trashed_posts = $this->get_trashed_landing_page_posts();
?>
<div class="e-landing-pages-empty">
<?php
/** @var Source_Local $source_local */
$source_local->print_blank_state_template( esc_html__( 'Landing Page', 'elementor' ), $this->get_add_new_landing_page_url(), esc_html__( 'Build Effective Landing Pages for your business\' marketing campaigns.', 'elementor' ) );
if ( ! empty( $trashed_posts ) ) : ?>
<div class="e-trashed-items">
<?php
printf(
/* translators: %1$s Link open tag, %2$s: Link close tag. */
esc_html__( 'Or view %1$sTrashed Items%1$s', 'elementor' ),
'<a href="' . esc_url( admin_url( 'edit.php?post_status=trash&post_type=' . self::CPT ) ) . '">',
'</a>'
);
?>
</div>
<?php endif; ?>
</div>
<?php
}
/**
* Is Current Admin Page Edit LP
*
* Checks whether the current page is a native WordPress edit page for a landing page.
*/
private function is_landing_page_admin_edit() {
$screen = get_current_screen();
if ( 'post' === $screen->base ) {
return $this->is_elementor_landing_page( get_post() );
}
return false;
}
/**
* Admin Localize Settings
*
* Enables adding properties to the globally available elementorAdmin.config JS object in the Admin Dashboard.
* Runs on the 'elementor/admin/localize_settings' filter.
*
* @since 3.1.0
*
* @param $settings
* @return array|null
*/
private function admin_localize_settings( $settings ) {
$additional_settings = [
'urls' => [
'addNewLandingPageUrl' => $this->get_add_new_landing_page_url(),
],
'landingPages' => [
'landingPagesHasPages' => $this->has_landing_pages(),
'isLandingPageAdminEdit' => $this->is_landing_page_admin_edit(),
],
];
return array_replace_recursive( $settings, $additional_settings );
}
/**
* Register Landing Pages CPT
*
* @since 3.1.0
*/
private function register_landing_page_cpt() {
$labels = [
'name' => esc_html__( 'Landing Pages', 'elementor' ),
'singular_name' => esc_html__( 'Landing Page', 'elementor' ),
'add_new' => esc_html__( 'Add New', 'elementor' ),
'add_new_item' => esc_html__( 'Add New Landing Page', 'elementor' ),
'edit_item' => esc_html__( 'Edit Landing Page', 'elementor' ),
'new_item' => esc_html__( 'New Landing Page', 'elementor' ),
'all_items' => esc_html__( 'All Landing Pages', 'elementor' ),
'view_item' => esc_html__( 'View Landing Page', 'elementor' ),
'search_items' => esc_html__( 'Search Landing Pages', 'elementor' ),
'not_found' => esc_html__( 'No landing pages found', 'elementor' ),
'not_found_in_trash' => esc_html__( 'No landing pages found in trash', 'elementor' ),
'parent_item_colon' => '',
'menu_name' => esc_html__( 'Landing Pages', 'elementor' ),
];
$args = [
'labels' => $labels,
'public' => true,
'show_in_menu' => 'edit.php?post_type=elementor_library&tabs_group=library',
'capability_type' => 'page',
'taxonomies' => [ Source_Local::TAXONOMY_TYPE_SLUG ],
'supports' => [ 'title', 'editor', 'comments', 'revisions', 'trackbacks', 'author', 'excerpt', 'page-attributes', 'thumbnail', 'custom-fields', 'post-formats', 'elementor' ],
];
register_post_type( self::CPT, $args );
}
/**
* Remove Post Type Slug
*
* Landing Pages are supposed to act exactly like pages. This includes their URLs being directly under the site's
* domain name. Since "Landing Pages" is a CPT, WordPress automatically adds the landing page slug as a prefix to
* it's posts' permalinks. This method checks if the post's post type is Landing Pages, and if it is, it removes
* the CPT slug from the requested post URL.
*
* Runs on the 'post_type_link' filter.
*
* @since 3.1.0
*
* @param $post_link
* @param $post
* @param $leavename
* @return string|string[]
*/
private function remove_post_type_slug( $post_link, $post, $leavename ) {
// Only try to modify the permalink if the post is a Landing Page.
if ( self::CPT !== $post->post_type || 'publish' !== $post->post_status ) {
return $post_link;
}
// Any slug prefixes need to be removed from the post link.
return trailingslashit( get_home_url() ) . trailingslashit( $post->post_name );
}
/**
* Adjust Landing Page Query
*
* Since Landing Pages are a CPT but should act like pages, the WP_Query that is used to fetch the page from the
* database needs to be adjusted. This method adds the Landing Pages CPT to the list of queried post types, to
* make sure the database query finds the correct Landing Page to display.
* Runs on the 'pre_get_posts' action.
*
* @since 3.1.0
*
* @param \WP_Query $query
*/
private function adjust_landing_page_query( \WP_Query $query ) {
// Only handle actual pages.
if (
! $query->is_main_query()
// If the query is not for a page.
|| ! isset( $query->query['page'] )
// If the query is for a static home/blog page.
|| is_home()
// If the post type comes already set, the main query is probably a custom one made by another plugin.
// In this case we do not want to intervene in order to not cause a conflict.
|| isset( $query->query['post_type'] )
) {
return;
}
// Create the post types property as an array and include the landing pages CPT in it.
$query_post_types = [ 'post', 'page', self::CPT ];
// Since WordPress determined this is supposed to be a page, we'll pre-set the post_type query arg to make sure
// it includes the Landing Page CPT, so when the query is parsed, our CPT will be a legitimate match to the
// Landing Page's permalink (that is directly under the domain, without a CPT slug prefix). In some cases,
// The 'name' property will be set, and in others it is the 'pagename', so we have to cover both cases.
if ( ! empty( $query->query['name'] ) ) {
$query->set( 'post_type', $query_post_types );
} elseif ( ! empty( $query->query['pagename'] ) && false === strpos( $query->query['pagename'], '/' ) ) {
$query->set( 'post_type', $query_post_types );
// We also need to set the name query var since redirect_guess_404_permalink() relies on it.
add_filter( 'pre_redirect_guess_404_permalink', function( $value ) use ( $query ) {
set_query_var( 'name', $query->query['pagename'] );
return $value;
} );
}
}
/**
* Handle 404
*
* This method runs after a page is not found in the database, but before a page is returned as a 404.
* These cases are handled in this filter callback, that runs on the 'pre_handle_404' filter.
*
* In some cases (such as when a site uses custom permalink structures), WordPress's WP_Query does not identify a
* Landing Page's URL as a post belonging to the Landing Page CPT. Some cases are handled successfully by the
* adjust_landing_page_query() method, but some are not and still trigger a 404 process. This method handles such
* cases by overriding the $wp_query global to fetch the correct landing page post entry.
*
* For example, since Landing Pages slugs come directly after the site domain name, WP_Query might parse the post
* as a category page. Since there is no category matching the slug, it triggers a 404 process. In this case, we
* run a query for a Landing Page post with the passed slug ($query->query['category_name']. If a Landing Page
* with the passed slug is found, we override the global $wp_query with the new, correct query.
*
* @param $current_value
* @param $query
* @return false
*/
private function handle_404( $current_value, $query ) {
global $wp_query;
// If another plugin/theme already used this filter, exit here to avoid conflicts.
if ( $current_value ) {
return $current_value;
}
if (
// Make sure we only intervene in the main query.
! $query->is_main_query()
// If a post was found, this is not a 404 case, so do not intervene.
|| ! empty( $query->posts )
// This filter is only meant to deal with wrong queries where the only query var is 'category_name'.
// If there is no 'category_name' query var, do not intervene.
|| empty( $query->query['category_name'] )
// If the query is for a real taxonomy (determined by it including a table to search in, such as the
// wp_term_relationships table), do not intervene.
|| ! empty( $query->tax_query->table_aliases )
) {
return false;
}
// Search for a Landing Page with the same name passed as the 'category name'.
$possible_new_query = new \WP_Query( [
'no_found_rows' => true,
'post_type' => self::CPT,
'name' => $query->query['category_name'],
] );
// Only if such a Landing Page is found, override the query to fetch the correct page.
if ( ! empty( $possible_new_query->posts ) ) {
$wp_query = $possible_new_query; //phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
}
return false;
}
public function __construct() {
$this->permalink_structure = get_option( 'permalink_structure' );
$this->register_landing_page_cpt();
// If there is a permalink structure set to the site, run the hooks that modify the Landing Pages permalinks to
// match WordPress' native 'Pages' post type.
if ( '' !== $this->permalink_structure ) {
// Landing Pages' post link needs to be modified to be identical to the pages permalink structure. This
// needs to happen in both the admin and the front end, since post links are also used in the admin pages.
add_filter( 'post_type_link', function( $post_link, $post, $leavename ) {
return $this->remove_post_type_slug( $post_link, $post, $leavename );
}, 10, 3 );
// The query itself only has to be manipulated when pages are viewed in the front end.
if ( ! is_admin() || wp_doing_ajax() ) {
add_action( 'pre_get_posts', function ( $query ) {
$this->adjust_landing_page_query( $query );
} );
// Handle cases where visiting a Landing Page's URL returns 404.
add_filter( 'pre_handle_404', function ( $value, $query ) {
return $this->handle_404( $value, $query );
}, 10, 2 );
}
}
add_action( 'elementor/documents/register', function( Documents_Manager $documents_manager ) {
$documents_manager->register_document_type( self::DOCUMENT_TYPE, Landing_Page::get_class_full_name() );
} );
add_action( 'elementor/admin/menu/register', function( Admin_Menu_Manager $admin_menu ) {
$this->register_admin_menu_legacy( $admin_menu );
}, Source_Local::ADMIN_MENU_PRIORITY + 20 );
// Add the custom 'Add New' link for Landing Pages into Elementor's admin config.
add_action( 'elementor/admin/localize_settings', function( array $settings ) {
return $this->admin_localize_settings( $settings );
} );
add_filter( 'elementor/template_library/sources/local/register_taxonomy_cpts', function( array $cpts ) {
$cpts[] = self::CPT;
return $cpts;
} );
// In the Landing Pages Admin Table page - Overwrite Template type column header title.
add_action( 'manage_' . Landing_Pages_Module::CPT . '_posts_columns', function( $posts_columns ) {
/** @var Source_Local $source_local */
$source_local = Plugin::$instance->templates_manager->get_source( 'local' );
return $source_local->admin_columns_headers( $posts_columns );
} );
// In the Landing Pages Admin Table page - Overwrite Template type column row values.
add_action( 'manage_' . Landing_Pages_Module::CPT . '_posts_custom_column', function( $column_name, $post_id ) {
/** @var Landing_Page $document */
$document = Plugin::$instance->documents->get( $post_id );
$document->admin_columns_content( $column_name );
}, 10, 2 );
// Overwrite the Admin Bar's 'New +' Landing Page URL with the link that creates the new LP in Elementor
// with the Template Library modal open.
add_action( 'admin_bar_menu', function( $admin_bar ) {
// Get the Landing Page menu node.
$new_landing_page_node = $admin_bar->get_node( 'new-e-landing-page' );
if ( $new_landing_page_node ) {
$new_landing_page_node->href = $this->get_add_new_landing_page_url();
$admin_bar->add_node( $new_landing_page_node );
}
}, 100 );
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace Elementor\Modules\LazyLoad;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Module extends BaseModule {
public function get_name() {
return 'lazyload';
}
public function __construct() {
parent::__construct();
add_action( 'init', [ $this, 'init' ] );
}
public function init() {
if ( ! $this->is_lazy_load_background_images_enabled() ) {
return;
}
add_action( 'wp_head', function() {
if ( ! $this->should_lazyload() ) {
return;
}
?>
<style>
.e-con.e-parent:nth-of-type(n+4):not(.e-lazyloaded):not(.e-no-lazyload),
.e-con.e-parent:nth-of-type(n+4):not(.e-lazyloaded):not(.e-no-lazyload) * {
background-image: none !important;
}
@media screen and (max-height: 1024px) {
.e-con.e-parent:nth-of-type(n+3):not(.e-lazyloaded):not(.e-no-lazyload),
.e-con.e-parent:nth-of-type(n+3):not(.e-lazyloaded):not(.e-no-lazyload) * {
background-image: none !important;
}
}
@media screen and (max-height: 640px) {
.e-con.e-parent:nth-of-type(n+2):not(.e-lazyloaded):not(.e-no-lazyload),
.e-con.e-parent:nth-of-type(n+2):not(.e-lazyloaded):not(.e-no-lazyload) * {
background-image: none !important;
}
}
</style>
<?php
} );
add_action( 'wp_footer', function() {
if ( ! $this->should_lazyload() ) {
return;
}
?>
<script type='text/javascript'>
const lazyloadRunObserver = () => {
const lazyloadBackgrounds = document.querySelectorAll( `.e-con.e-parent:not(.e-lazyloaded)` );
const lazyloadBackgroundObserver = new IntersectionObserver( ( entries ) => {
entries.forEach( ( entry ) => {
if ( entry.isIntersecting ) {
let lazyloadBackground = entry.target;
if( lazyloadBackground ) {
lazyloadBackground.classList.add( 'e-lazyloaded' );
}
lazyloadBackgroundObserver.unobserve( entry.target );
}
});
}, { rootMargin: '200px 0px 200px 0px' } );
lazyloadBackgrounds.forEach( ( lazyloadBackground ) => {
lazyloadBackgroundObserver.observe( lazyloadBackground );
} );
};
const events = [
'DOMContentLoaded',
'elementor/lazyload/observe',
];
events.forEach( ( event ) => {
document.addEventListener( event, lazyloadRunObserver );
} );
</script>
<?php
} );
}
private function should_lazyload() {
return ! is_admin() && ! Plugin::$instance->preview->is_preview_mode() && ! Plugin::$instance->editor->is_edit_mode();
}
private static function is_lazy_load_background_images_enabled(): bool {
return '1' === get_option( 'elementor_lazy_load_background_images', '1' );
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Elementor\Modules\Library\Documents;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Elementor container library document.
*
* Elementor container library document handler class is responsible for
* handling a document of a container type.
*
* @since 2.0.0
*/
class Container extends Library_Document {
public static function get_properties() {
$properties = parent::get_properties();
$properties['support_kit'] = true;
return $properties;
}
/**
* Get document name.
*
* Retrieve the document name.
*
* @since 2.0.0
* @access public
*
* @return string Document name.
*/
public function get_name() {
return 'container';
}
/**
* Get document title.
*
* Retrieve the document title.
*
* @since 2.0.0
* @access public
* @static
*
* @return string Document title.
*/
public static function get_title() {
return esc_html__( 'Container', 'elementor' );
}
/**
* Get Type
*
* Return the container document type.
*
* @return string
*/
public static function get_type() {
return 'container';
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Elementor\Modules\Library\Documents;
use Elementor\Core\Base\Document;
use Elementor\Modules\Library\Traits\Library;
use Elementor\TemplateLibrary\Source_Local;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Elementor library document.
*
* Elementor library document handler class is responsible for handling
* a document of the library type.
*
* @since 2.0.0
*/
abstract class Library_Document extends Document {
// Library Document Trait
use Library;
/**
* The taxonomy type slug for the library document.
*/
const TAXONOMY_TYPE_SLUG = 'elementor_library_type';
/**
* Get document properties.
*
* Retrieve the document properties.
*
* @since 2.0.0
* @access public
* @static
*
* @return array Document properties.
*/
public static function get_properties() {
$properties = parent::get_properties();
$properties['admin_tab_group'] = 'library';
$properties['show_in_library'] = true;
$properties['register_type'] = true;
$properties['cpt'] = [ Source_Local::CPT ];
return $properties;
}
/**
* Get initial config.
*
* Retrieve the current element initial configuration.
*
* Adds more configuration on top of the controls list and the tabs assigned
* to the control. This method also adds element name, type, icon and more.
*
* @since 2.9.0
* @access protected
*
* @return array The initial config.
*/
public function get_initial_config() {
$config = parent::get_initial_config();
$config['library'] = [
'save_as_same_type' => true,
];
return $config;
}
public function get_content( $with_css = false ) {
return do_shortcode( parent::get_content( $with_css ) );
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Elementor\Modules\Library\Documents;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Elementor section library document.
*
* Elementor section library document handler class is responsible for
* handling a document of a section type.
*
*/
class Not_Supported extends Library_Document {
/**
* Get document properties.
*
* Retrieve the document properties.
*
* @access public
* @static
*
* @return array Document properties.
*/
public static function get_properties() {
$properties = parent::get_properties();
$properties['admin_tab_group'] = '';
$properties['register_type'] = false;
$properties['is_editable'] = false;
$properties['show_in_library'] = false;
$properties['show_in_finder'] = false;
return $properties;
}
public static function get_type() {
return 'not-supported';
}
/**
* Get document title.
*
* Retrieve the document title.
*
* @access public
* @static
*
* @return string Document title.
*/
public static function get_title() {
return esc_html__( 'Not Supported', 'elementor' );
}
public function save_template_type() {
// Do nothing.
}
public function print_admin_column_type() {
Utils::print_unescaped_internal_string( self::get_title() );
}
public function filter_admin_row_actions( $actions ) {
unset( $actions['view'] );
return $actions;
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace Elementor\Modules\Library\Documents;
use Elementor\Core\DocumentTypes\Post;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Elementor page library document.
*
* Elementor page library document handler class is responsible for
* handling a document of a page type.
*
* @since 2.0.0
*/
class Page extends Library_Document {
/**
* Get document properties.
*
* Retrieve the document properties.
*
* @since 2.0.0
* @access public
* @static
*
* @return array Document properties.
*/
public static function get_properties() {
$properties = parent::get_properties();
$properties['support_wp_page_templates'] = true;
$properties['support_kit'] = true;
$properties['show_in_finder'] = true;
return $properties;
}
public static function get_type() {
return 'page';
}
/**
* Get document title.
*
* Retrieve the document title.
*
* @since 2.0.0
* @access public
* @static
*
* @return string Document title.
*/
public static function get_title() {
return esc_html__( 'Page', 'elementor' );
}
public static function get_plural_title() {
return esc_html__( 'Pages', 'elementor' );
}
public static function get_add_new_title() {
return esc_html__( 'Add New Page Template', 'elementor' );
}
/**
* @since 2.1.3
* @access public
*/
public function get_css_wrapper_selector() {
return 'body.elementor-page-' . $this->get_main_id();
}
/**
* @since 3.1.0
* @access protected
*/
protected function register_controls() {
parent::register_controls();
Post::register_hide_title_control( $this );
Post::register_style_controls( $this );
}
protected function get_remote_library_config() {
$config = parent::get_remote_library_config();
$config['type'] = 'page';
$config['default_route'] = 'templates/pages';
return $config;
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Elementor\Modules\Library\Documents;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Elementor section library document.
*
* Elementor section library document handler class is responsible for
* handling a document of a section type.
*
* @since 2.0.0
*/
class Section extends Library_Document {
public static function get_properties() {
$properties = parent::get_properties();
$properties['support_kit'] = true;
$properties['show_in_finder'] = true;
return $properties;
}
public static function get_type() {
return 'section';
}
/**
* Get document title.
*
* Retrieve the document title.
*
* @since 2.0.0
* @access public
* @static
*
* @return string Document title.
*/
public static function get_title() {
return esc_html__( 'Section', 'elementor' );
}
public static function get_plural_title() {
return esc_html__( 'Sections', 'elementor' );
}
}

Some files were not shown because too many files have changed in this diff Show More