first commit

This commit is contained in:
Roman Pyrih
2026-03-10 09:50:10 +01:00
commit 64c4a90405
7289 changed files with 2645777 additions and 0 deletions

View File

@@ -0,0 +1,166 @@
<?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.
// We should add dependencies to make sure that 'elementor-admin-bar' is loaded before 'admin-bar'.
wp_enqueue_script(
'admin-bar',
null,
[ 'elementor-admin-bar' ],
false, // phpcs:ignore WordPress.WP.EnqueuedResourceParameters
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 ),
];
}
public function add_clear_cache_in_admin_bar( $admin_bar_config ): array {
if ( current_user_can( 'manage_options' ) ) {
$clear_cache_url = add_query_arg(
[
'_wpnonce' => wp_create_nonce( 'elementor_site_clear_cache' ),
],
admin_url( 'admin-post.php?action=elementor_site_clear_cache' ),
);
$admin_bar_config['elementor_edit_page']['children'][] = [
'id' => 'elementor_site_clear_cache',
'title' => esc_html__( 'Clear Files & Data', 'elementor' ),
'sub_title' => esc_html__( 'Site', 'elementor' ),
'href' => $clear_cache_url,
];
}
return $admin_bar_config;
}
/**
* 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 */ );
add_filter( 'elementor/frontend/admin_bar/settings', [ $this, 'add_clear_cache_in_admin_bar' ], 500 );
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace Elementor\Modules\AdminTopBar;
use Elementor\Core\Admin\Admin;
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' ), [], 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();
return apply_filters(
'elementor/admin-top-bar/is-active',
Admin::is_elementor_admin_page( $current_screen ),
$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,906 @@
<?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 IMAGE_BACKGROUND_COLOR = 'background_color';
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_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'
);
}
/**
* @param array $event_data {
* @type string $name
* @type array $data
* @type array $client {
* @type string $name
* @type string $version
* @type string $session_id
* }
* }
*/
public function send_event( array $event_data ): void {
$this->ai_request(
'POST',
'client-events/events',
[
'payload' => $event_data,
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
],
false,
'',
'json'
);
}
/**
* Get file upload 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 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 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 get_featured_image
*
* @param $data
* @param $context
* @param $request_ids
* @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 get_image_to_image
*
* @param $image_data
* @param $context
* @param $request_ids
* @return mixed|\WP_Error
* @throws \Exception If image file not found.
*/
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;
}
private function resizeImageIfNeeded( $original_url ) {
try {
$max_file_size = 4194304;
$current_size = filesize( $original_url );
if ( $current_size <= $max_file_size ) {
return $original_url;
}
$image_editor = wp_get_image_editor( $original_url );
if ( is_wp_error( $image_editor ) ) {
return $original_url;
}
$dimensions = $image_editor->get_size();
$original_width = $dimensions['width'];
$original_height = $dimensions['height'];
$scaling_factor = sqrt( $max_file_size / $current_size );
$new_width = (int) ( $original_width * $scaling_factor );
$new_height = (int) ( $original_height * $scaling_factor );
$image_editor->resize( $new_width, $new_height, true );
$file_extension = pathinfo( $original_url, PATHINFO_EXTENSION );
$temp_image = tempnam( sys_get_temp_dir(), 'resized_' ) . '.' . $file_extension;
$image_editor->save( $temp_image );
return $temp_image;
} catch ( \Exception $e ) {
return $original_url;
}
}
public function get_unify_product_images( $image_data, $context, $request_ids ) {
$image_file = get_attached_file( $image_data['attachment_id'] );
if ( ! $image_file ) {
throw new \Exception( 'Image file not found' );
}
$final_path = $this->resizeImageIfNeeded( $image_file );
$result = $this->ai_request(
'POST',
'image/image-to-image/unify-product-images',
[
'aspectRatio' => $image_data['promptSettings'][ self::ASPECT_RATIO ],
'backgroundColor' => $image_data['promptSettings'][ self::IMAGE_BACKGROUND_COLOR ],
'featureIdentifier' => $image_data['featureIdentifier'],
'context' => wp_json_encode( $context ),
'ids' => $request_ids,
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
],
$final_path,
'image'
);
if ( $image_file !== $final_path ) {
unlink( $final_path );
}
return $result;
}
/**
* Get Image To Image Upscale get_image_to_image_upscale
*
* @param $image_data
* @param $context
* @param $request_ids
* @return mixed|\WP_Error
* @throws \Exception If image file not found.
*/
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 get_image_to_image_remove_background
*
* @param $image_data
* @param $context
* @param $request_ids
* @return mixed|\WP_Error
* @throws \Exception If image file not found.
*/
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 get_image_to_image_remove_text
*
* @param $image_data
* @param $context
* @param $request_ids
* @return mixed|\WP_Error
* @throws \Exception If image file not found.
*/
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 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 get_image_to_image_out_painting
*
* @param $image_data
* @param $context
* @param $request_ids
* @return mixed|\WP_Error
* @throws \Exception If image file not found.
*/
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 get_image_to_image_mask
*
* @param $image_data
* @param $context
* @param $request_ids
* @return mixed|\WP_Error
* @throws \Exception If image file not found.
*/
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->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 = [] ) {
$sanitized_id = str_replace( '%', '%%', $id );
return $this->ai_request(
'POST', sprintf( 'history/%s/favorite', $sanitized_id ),
[
'context' => wp_json_encode( $context ),
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
]
);
}
public function get_animation( $data, $context, $request_ids ) {
return $this->ai_request(
'POST',
'text/get-motion-effect',
[
'prompt' => $data['payload']['prompt'],
'motionEffectType' => $data['payload']['motionEffectType'],
'context' => wp_json_encode( $context ),
'ids' => $request_ids,
'api_version' => ELEMENTOR_VERSION,
'site_lang' => get_bloginfo( 'language' ),
],
false,
'',
'json'
);
}
protected function init() {}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Elementor\Modules\Ai\Feature_Intro;
use Elementor\Core\Upgrade\Manager as Upgrade_Manager;
use Elementor\User;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Product_Image_Unification_Intro {
const RELEASE_VERSION = '3.26.0';
const CURRENT_POINTER_SLUG = 'e-ai-product-image-unification';
public static function add_hooks() {
add_action( 'admin_print_footer_scripts', [ __CLASS__, 'product_image_unification_intro_script' ] );
}
public static function product_image_unification_intro_script() {
if ( static::is_dismissed() ) {
return;
}
$screen = get_current_screen();
if ( ! isset( $screen->post_type ) || 'product' !== $screen->post_type ) {
return;
}
wp_enqueue_script( 'wp-pointer' );
wp_enqueue_style( 'wp-pointer' );
$pointer_content = '<h3>' . esc_html__( 'New! Unify pack-shots with Elementor AI', 'elementor' ) . '</h3>';
$pointer_content .= '<p>' . esc_html__( 'Now you can process images in bulk and standardized the background and ratio - no manual editing required!', 'elementor' ) . '</p>';
$pointer_content .= sprintf(
'<p><button style="padding: 0; border: 0"><a class="button button-primary" href="%s" target="_blank">%s</a></button></p>',
esc_js( 'https://go.elementor.com/wp-dash-unify-images-learn-more/' ),
esc_html__( 'Learn more', 'elementor' )
);
?>
<script>
jQuery( document ).ready( function( $ ) {
setTimeout( function () {
$( '#bulk-action-selector-top' ).pointer( {
content: '<?php echo $pointer_content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>',
position: {
edge: <?php echo is_rtl() ? "'right'" : "'left'"; ?>,
align: 'center'
},
pointerWidth: 360,
close: function () {
elementorCommon.ajax.addRequest( 'introduction_viewed', {
data: {
introductionKey: '<?php echo esc_attr( static::CURRENT_POINTER_SLUG ); ?>',
},
} );
}
} ).pointer( 'open' );
}, 10 );
} );
</script>
<?php
}
private static function is_dismissed() {
return User::get_introduction_meta( static::CURRENT_POINTER_SLUG );
}
}

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,57 @@
<?php
namespace Elementor\Modules\Ai\SitePlannerConnect;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module {
const NOT_TRANSLATED_APP_NAME = 'Site Planner';
const PLANNER_ORIGIN = 'https://planner.elementor.com';
const HIDDEN_PAGE_SLUG = '';
public function __construct() {
add_action( 'rest_api_init', [ $this, 'on_rest_init' ] );
add_action( 'admin_menu', [ $this, 'register_menu_page' ], 100 );
add_filter( 'rest_prepare_application_password', function ( $response, $item, $request ) {
if ( '/wp/v2/users/me/application-passwords' === $request->get_route() && is_user_logged_in() ) {
$user = wp_get_current_user();
$response->data['user_login'] = $user->user_login;
}
return $response;
}, 10, 3 );
}
public function on_rest_init(): void {
( new Wp_Rest_Api() )->register();
}
public function register_menu_page() {
add_submenu_page(
self::HIDDEN_PAGE_SLUG,
'App Password Generator',
'App Password',
'manage_options',
'e-site-planner-password-generator',
[ $this, 'render_menu_page' ]
);
}
public function render_menu_page() {
ob_start();
require_once __DIR__ . '/view.php';
$content = ob_get_clean();
$vars = [
'%app_name%' => self::NOT_TRANSLATED_APP_NAME,
'%safe_origin%' => esc_url( self::PLANNER_ORIGIN ),
'%domain%' => isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '',
'%title%' => esc_html__( 'Connect to Site Planner', 'elementor' ),
'%description%' => esc_html__( 'To connect your site to Site Planner, you need to generate an app password.', 'elementor' ),
'%cta%' => esc_html__( 'Approve & Connect', 'elementor' ),
];
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo strtr( $content, $vars );
}
}

View File

@@ -0,0 +1,198 @@
<?php
namespace Elementor\Modules\Ai\SitePlannerConnect;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&display=swap"
rel="stylesheet"><?php // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet ?>
<style>
#wpwrap {
display: none;
}
.site-planner-consent {
position: fixed;
top: 0;
left: 0;
z-index: 99999; /* above admin top bar */
width: 100%;
background-color: #fff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
}
.site-planner-consent-title {
color: #0C0D0E;
text-align: center;
/* typography/h4 */
font-family: Roboto, sans-serif;
font-size: 32px;
font-style: normal;
font-weight: 700;
line-height: 123.5%;
letter-spacing: 0.25px;
}
.site-planner-consent-description {
width: 393px;
color: #69727D;
text-align: center;
/* typography/body1 */
font-family: Roboto, sans-serif;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 150%; /* 24px */
letter-spacing: 0.15px;
}
.site-planner-consent-connect-names {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 500px;
}
.site-planner-consent-connect-names div {
width: 50%;
text-align: center;
}
.site-planner-consent button {
cursor: pointer;
border: none;
display: flex;
width: 387px;
padding: 8px 22px;
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: 4px;
background: #F0ABFC;
color: #0C0D0E;
font-feature-settings: 'liga' off, 'clig' off;
/* components/button/button-large */
font-family: Roboto, sans-serif;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 26px; /* 162.5% */
letter-spacing: 0.46px;
}
.site-planner-consent .generating-results {
display: none;
padding: 8px 16px;
margin: 0 32px;
}
.site-planner-consent .generating-results.error {
display: block;
background: rgb(253, 236, 236);
}
</style>
<div class="site-planner-consent">
<h1 class="site-planner-consent-title">
%title%
</h1>
<div style="height: 20px"></div>
<p class="site-planner-consent-description">
%description%
</p>
<div style="height: 40px"></div>
<svg width="287" height="40" viewBox="0 0 287 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="16.5" y1="22.5" x2="271.5" y2="22.5" stroke="#69727D" stroke-linecap="round" stroke-linejoin="round"
stroke-dasharray="2 4"/>
<circle cx="145.623" cy="22" r="11.5" fill="white" stroke="#69727D"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M147.977 19.6467C148.172 19.842 148.172 20.1586 147.977 20.3538L143.977 24.3538C143.782 24.5491 143.465 24.5491 143.27 24.3538C143.074 24.1586 143.074 23.842 143.27 23.6467L147.27 19.6467C147.465 19.4515 147.782 19.4515 147.977 19.6467Z"
fill="#69727D"/>
<path
d="M149.691 18.1948L149.402 17.9058C148.377 16.8804 146.714 16.8804 145.689 17.9058L145.002 18.5922C144.807 18.7875 144.491 18.7875 144.295 18.5922C144.1 18.397 144.1 18.0804 144.295 17.8851L144.982 17.1987C146.398 15.7827 148.693 15.7827 150.109 17.1987L150.398 17.4877C151.814 18.9036 151.814 21.1993 150.398 22.6153L149.712 23.3017C149.517 23.497 149.2 23.497 149.005 23.3017C148.81 23.1065 148.81 22.7899 149.005 22.5946L149.691 21.9082C150.717 20.8828 150.717 19.2202 149.691 18.1948Z"
fill="#69727D"/>
<path
d="M141.529 22.0658C140.503 23.0912 140.503 24.7538 141.529 25.7792L141.818 26.0682C142.843 27.0936 144.506 27.0936 145.531 26.0682L146.218 25.3818C146.413 25.1865 146.73 25.1865 146.925 25.3818C147.12 25.577 147.12 25.8936 146.925 26.0889L146.238 26.7753C144.822 28.1913 142.527 28.1913 141.111 26.7753L140.822 26.4863C139.406 25.0704 139.406 22.7747 140.822 21.3587L141.508 20.6723C141.703 20.477 142.02 20.477 142.215 20.6723C142.411 20.8675 142.411 21.1841 142.215 21.3794L141.529 22.0658Z"
fill="#69727D"/>
<rect x="247" width="40" height="40" rx="20" fill="#F3F3F4"/>
<g clip-path="url(#clip0_7635_41076)">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M257.022 26.6668C255.704 24.6934 255 22.3734 255 20C255 16.8174 256.264 13.7652 258.515 11.5147C260.765 9.26428 263.817 8 267 8C269.373 8 271.693 8.70379 273.667 10.0224C275.64 11.3409 277.178 13.2151 278.087 15.4078C278.995 17.6005 279.232 20.0133 278.769 22.3411C278.306 24.6688 277.164 26.807 275.485 28.4853C273.807 30.1635 271.669 31.3064 269.341 31.7694C267.013 32.2324 264.601 31.9948 262.408 31.0865C260.215 30.1783 258.341 28.6402 257.022 26.6668ZM264 14.9996H262.001V24.9999H264V14.9996ZM271.999 14.9996H266V16.9993H271.999V14.9996ZM271.999 18.999H266V20.9987H271.999V18.999ZM271.999 23.0002H266V24.9999H271.999V23.0002Z"
fill="#0C0D0E"/>
</g>
<rect width="40" height="40" rx="20" fill="#F3F3F4"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M20.0004 10.0156C14.4944 10.0156 10.0156 14.494 10.0156 19.9996C10.0156 25.5053 14.4944 29.9844 20.0004 29.9844C25.5056 29.9844 29.9844 25.5053 29.9844 19.9996C29.9844 14.4947 25.5056 10.0156 20.0004 10.0156ZM11.1616 19.9996C11.1616 18.7184 11.4367 17.5017 11.927 16.4031L16.1431 27.9539C13.1948 26.5215 11.1616 23.4984 11.1616 19.9996ZM20.0004 28.8387C19.1327 28.8387 18.2954 28.7106 17.5032 28.4785L20.1549 20.7731L22.8725 28.2154C22.8898 28.2589 22.9115 28.2992 22.9353 28.3372C22.0167 28.6607 21.0292 28.8387 20.0004 28.8387ZM21.218 15.856C21.7501 15.8279 22.2293 15.7715 22.2293 15.7715C22.7058 15.7153 22.65 15.0158 22.1733 15.0438C22.1733 15.0438 20.7415 15.156 19.8176 15.156C18.9495 15.156 17.4894 15.0438 17.4894 15.0438C17.0133 15.0158 16.9579 15.744 17.4336 15.7715C17.4336 15.7715 17.8845 15.8277 18.3602 15.856L19.7373 19.6286L17.8034 25.4297L14.5851 15.8564C15.1178 15.8283 15.5968 15.7721 15.5968 15.7721C16.0725 15.7159 16.0169 15.016 15.54 15.0445C15.54 15.0445 14.1088 15.1564 13.1843 15.1564C13.0178 15.1564 12.823 15.1521 12.6157 15.1457C14.1954 12.7459 16.9123 11.1617 20.0004 11.1617C22.3018 11.1617 24.3964 12.0416 25.9689 13.4816C25.9302 13.4797 25.8937 13.4748 25.854 13.4748C24.9861 13.4748 24.3695 14.2309 24.3695 15.0434C24.3695 15.7715 24.789 16.388 25.2377 17.1159C25.5741 17.7051 25.9662 18.4613 25.9662 19.5537C25.9662 20.3102 25.6758 21.1882 25.2936 22.4107L24.4121 25.3566L21.218 15.856ZM24.4435 27.6389L27.1431 19.8337C27.6481 18.573 27.8152 17.5647 27.8152 16.6679C27.8152 16.343 27.7937 16.0404 27.7557 15.7591C28.4466 17.018 28.8391 18.4629 28.8386 19.9998C28.8386 23.2602 27.0708 26.1068 24.4435 27.6389Z"
fill="#0C0D0E"/>
<defs>
<clipPath id="clip0_7635_41076">
<rect width="24" height="24" fill="white" transform="translate(255 8)"/>
</clipPath>
</defs>
</svg>
<div class="site-planner-consent-connect-names">
<div>%domain%</div>
<div>%app_name%</div>
</div>
<div style="height: 40px"></div>
<button class="site-planner-consent-button" onclick="sendPassword()">
%cta%
</button>
<div style="height: 40px"></div>
<div class="generating-results"></div>
</div>
<script>
const generatingResults = document.querySelector(".generating-results");
const hideAdminUi = () => {
document.body.append(document.querySelector(".site-planner-consent"))
}
const sendPassword = () => {
generatingResults.classList.remove("error");
fetch(`${ wpApiSettings.root}wp/v2/users/me/application-passwords`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-WP-Nonce": wpApiSettings.nonce,
},
body: JSON.stringify({
name: "Site Planner Connect"
})
})
.then(response => response.json())
.then(data => {
window.opener.postMessage({
type: "app_password",
details: {
userLogin: data.user_login,
appPassword: data.password,
uuid: data.uuid,
created: data.created
}
}, '%safe_origin%');
window.close();
})
.catch(error => {
console.error("Error:", error);
generatingResults.classList.add("error");
generatingResults.innerText = "Error generating password: " + error;
});
}
hideAdminUi();
</script>

View File

@@ -0,0 +1,35 @@
<?php
namespace Elementor\Modules\Ai\SitePlannerConnect;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Just a simple rest api to validate new Site Planner Connect feature exists.
*/
class Wp_Rest_Api {
public function register(): void {
register_rest_route('elementor-ai/v1', 'permissions', [
[
'methods' => \WP_REST_Server::READABLE,
'permission_callback' => function () {
return current_user_can( 'manage_options' );
},
'callback' => function () {
try {
wp_send_json_success( [
'site_planner_connect' => true,
] );
} catch ( \Exception $e ) {
wp_send_json_error( [
'message' => $e->getMessage(),
] );
}
},
],
] );
}
}

View File

@@ -0,0 +1,77 @@
<?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 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,186 @@
<?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 {
const AI_ASSETS_BASE_URL = 'https://assets.elementor.com/ai/v1/';
/**
* @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' => self::AI_ASSETS_BASE_URL . 'images/ai-social-hd.gif',
],
'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,189 @@
<?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'] = '#';
}
} elseif ( 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,111 @@
<?php
namespace Elementor\Modules\Apps;
use Elementor\Core\Admin\Menu\Admin_Menu_Manager;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Plugin;
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 ) {
if ( ! Plugin::instance()->modules_manager->get_modules( 'editor-one' ) ) {
$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,54 @@
<?php
namespace Elementor\Modules\AtomicOptIn;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Experiments\Manager as Experiments_Manager;
use Elementor\Modules\AtomicWidgets\OptIn\Opt_In as Atomic_Widgets_Opt_In;
use Elementor\Plugin;
class Module extends BaseModule {
const EXPERIMENT_NAME = 'e_opt_in_v4_page';
const MODULE_NAME = 'editor-v4-opt-in';
const WELCOME_POPOVER_DISPLAYED_OPTION = '_e_welcome_popover_displayed';
public function get_name() {
return 'atomic-opt-in';
}
public static function get_experimental_data(): array {
return [
'name' => self::EXPERIMENT_NAME,
'title' => esc_html__( 'Editor v4 (Opt In Page)', 'elementor' ),
'description' => esc_html__( 'Enable the settings Opt In page', 'elementor' ),
'hidden' => true,
'default' => Experiments_Manager::STATE_ACTIVE,
'release_status' => Experiments_Manager::RELEASE_STATUS_ALPHA,
];
}
public function get_opt_in_css_assets_url( string $path ) {
return $this->get_css_assets_url( $path );
}
public function __construct() {
( new PanelChip() )->init();
if ( ! Plugin::$instance->experiments->is_feature_active( self::EXPERIMENT_NAME ) ) {
return;
}
( new Atomic_Widgets_Opt_In() )->init();
( new OptInPage( $this ) )->init();
if ( ! $this->is_atomic_experiment_active() ) {
return;
}
( new WelcomeScreen() )->init();
}
public function is_atomic_experiment_active(): bool {
return Plugin::$instance->experiments->is_feature_active( Atomic_Widgets_Opt_In::EXPERIMENT_NAME );
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace Elementor\Modules\AtomicOptIn;
use Elementor\Plugin;
use Elementor\Settings;
use Elementor\User;
use Elementor\Utils;
class OptInPage {
private Module $module;
public function __construct( Module $module ) {
$this->module = $module;
}
public function init() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$this->register_assets();
$this->add_settings_tab();
}
private function register_assets() {
$page_id = Settings::PAGE_ID;
add_action( "elementor/admin/after_create_settings/{$page_id}", [ $this, 'enqueue_scripts' ] );
add_action( "elementor/admin/after_create_settings/{$page_id}", [ $this, 'enqueue_styles' ] );
}
public function enqueue_styles() {
wp_enqueue_style(
Module::MODULE_NAME,
$this->module->get_opt_in_css_assets_url( 'modules/editor-v4-opt-in/opt-in' ),
[],
ELEMENTOR_VERSION
);
}
public function enqueue_scripts() {
$min_suffix = Utils::is_script_debug() ? '' : '.min';
wp_enqueue_script(
Module::MODULE_NAME,
ELEMENTOR_ASSETS_URL . 'js/editor-v4-opt-in' . $min_suffix . '.js',
[
'react',
'react-dom',
'elementor-common',
'elementor-v2-ui',
],
ELEMENTOR_VERSION,
true
);
wp_localize_script(
Module::MODULE_NAME,
'elementorSettingsEditor4OptIn',
$this->prepare_data()
);
wp_set_script_translations( Module::MODULE_NAME, 'elementor' );
}
private function prepare_data() {
$create_new_post_type = User::is_current_user_can_edit_post_type( 'page' ) ? 'page' : 'post';
return [
'features' => [
'editor_v4' => $this->module->is_atomic_experiment_active(),
],
'urls' => [
'start_building' => esc_url( Plugin::$instance->documents->get_create_new_post_url( $create_new_post_type ) ),
],
];
}
private function add_settings_tab() {
$page_id = Settings::PAGE_ID;
add_action( "elementor/admin/after_create_settings/{$page_id}", function( Settings $settings ) {
$this->add_new_tab_to( $settings );
}, 11 );
}
private function add_new_tab_to( Settings $settings ) {
$settings->add_tab( Module::MODULE_NAME, [
'label' => esc_html__( 'Version 4', 'elementor' ),
'sections' => [
'opt-in' => [
'callback' => function() {
echo '<div id="page-editor-v4-opt-in"></div>';
},
'fields' => [],
],
],
] );
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Elementor\Modules\AtomicOptIn;
use Elementor\Utils;
class PanelChip {
public function init() {
add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
}
public function enqueue_scripts() {
$min_suffix = Utils::is_script_debug() ? '' : '.min';
wp_enqueue_script(
'editor-v4-opt-in-alphachip',
ELEMENTOR_ASSETS_URL . 'js/editor-v4-opt-in-alphachip' . $min_suffix . '.js',
[
'react',
'react-dom',
'elementor-common',
'elementor-v2-ui',
],
ELEMENTOR_VERSION,
true
);
wp_set_script_translations( 'editor-v4-opt-in-alphachip', 'elementor' );
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Elementor\Modules\AtomicOptIn;
use Elementor\Core\Isolation\Elementor_Adapter;
use Elementor\Core\Isolation\Elementor_Adapter_Interface;
use Elementor\Modules\ElementorCounter\Module as Elementor_Counter;
use Elementor\Utils;
class WelcomeScreen {
private Elementor_Adapter_Interface $elementor_adapter;
public function __construct() {
$this->elementor_adapter = new Elementor_Adapter();
}
public function init() {
add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'maybe_enqueue_welcome_popover' ] );
}
public function maybe_enqueue_welcome_popover(): void {
if ( $this->is_first_or_second_editor_visit() ) {
return;
}
if ( $this->has_welcome_popover_been_displayed() ) {
return;
}
$this->enqueue_scripts();
$this->set_welcome_popover_as_displayed();
}
private function is_first_or_second_editor_visit(): bool {
if ( ! $this->elementor_adapter ) {
return false;
}
$editor_visit_count = $this->elementor_adapter->get_count( Elementor_Counter::EDITOR_COUNTER_KEY );
return $editor_visit_count < 3;
}
private function has_welcome_popover_been_displayed(): bool {
return get_user_meta( $this->get_current_user_id(), Module::WELCOME_POPOVER_DISPLAYED_OPTION, true );
}
private function set_welcome_popover_as_displayed(): void {
update_user_meta( $this->get_current_user_id(), Module::WELCOME_POPOVER_DISPLAYED_OPTION, true );
}
private function enqueue_scripts() {
$min_suffix = Utils::is_script_debug() ? '' : '.min';
wp_enqueue_script(
Module::MODULE_NAME . '-welcome',
ELEMENTOR_ASSETS_URL . 'js/editor-v4-welcome-opt-in' . $min_suffix . '.js',
[
'react',
'react-dom',
'elementor-common',
'elementor-v2-ui',
],
ELEMENTOR_VERSION,
true
);
wp_set_script_translations( Module::MODULE_NAME . '-welcome', 'elementor' );
}
private function get_current_user_id(): int {
$current_user = wp_get_current_user();
return $current_user->ID ?? 0;
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Base;
use Elementor\Modules\AtomicWidgets\Controls\Base\Atomic_Control_Base as New_Atomic_Control_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
// TODO: Remove this class after 3.36 is released.
/**
* @deprecated 3.34 Use \Elementor\Modules\AtomicWidgets\Controls\Base\Atomic_Control_Base instead.
*/
abstract class Atomic_Control_Base extends New_Atomic_Control_Base {}

View File

@@ -0,0 +1,15 @@
<?php
namespace Elementor\Modules\AtomicWidgets\CacheValidity;
use Elementor\Modules\AtomicWidgets\Styles\CacheValidity\Cache_Validity_Item as New_Cache_Validity_Item;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
// TODO: Remove this class after 3.37 is released.
/**
* @deprecated 3.35 Use \Elementor\Modules\AtomicWidgets\Styles\CacheValidity\Cache_Validity_Item instead.
*/
class Cache_Validity_Item extends New_Cache_Validity_Item {}

View File

@@ -0,0 +1,15 @@
<?php
namespace Elementor\Modules\AtomicWidgets\CacheValidity;
use Elementor\Modules\AtomicWidgets\Styles\CacheValidity\Cache_Validity as New_Cache_Validity;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
// TODO: Remove this class after 3.37 is released.
/**
* @deprecated 3.35 Use \Elementor\Modules\AtomicWidgets\Styles\CacheValidity\Cache_Validity instead.
*/
class Cache_Validity extends New_Cache_Validity {}

View File

@@ -0,0 +1,64 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls\Base;
use JsonSerializable;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Atomic_Control_Base implements JsonSerializable {
private string $bind;
private $label = null;
private $description = null;
private $meta = 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 = html_entity_decode( $label );
return $this;
}
public function set_description( string $description ): self {
$this->description = html_entity_decode( $description );
return $this;
}
public function set_meta( $meta ): self {
$this->meta = $meta;
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(),
'meta' => $this->meta,
],
];
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls\Base;
use JsonSerializable;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Element_Control_Base implements JsonSerializable {
private $label = null;
private $meta = null;
abstract public function get_type(): string;
abstract public function get_props(): array;
public static function make(): self {
return new static();
}
public function set_label( string $label ): self {
$this->label = $label;
return $this;
}
public function get_label(): string {
return $this->label;
}
public function set_meta( $meta ): self {
$this->meta = $meta;
return $this;
}
public function get_meta(): array {
return $this->meta;
}
public function jsonSerialize(): array {
return [
'type' => 'element-control',
'value' => [
'label' => $this->get_label(),
'meta' => $this->get_meta(),
'type' => $this->get_type(),
'props' => $this->get_props(),
],
];
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls;
use JsonSerializable;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Section implements JsonSerializable {
private ?string $id = null;
private $label = null;
private $description = null;
private array $items = [];
public static function make(): self {
return new static();
}
public function set_id( string $id ): self {
$this->id = $id;
return $this;
}
public function get_id() {
return $this->id;
}
public function set_label( string $label ): self {
$this->label = html_entity_decode( $label );
return $this;
}
public function get_label(): ?string {
return $this->label;
}
public function set_description( string $description ): self {
$this->description = html_entity_decode( $description );
return $this;
}
public function set_items( array $items ): self {
$this->items = $items;
return $this;
}
public function add_item( $item ): self {
$this->items[] = $item;
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,18 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls\Types;
use Elementor\Modules\AtomicWidgets\Controls\Base\Atomic_Control_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Date_Time_Control extends Atomic_Control_Base {
public function get_type(): string {
return 'date-time';
}
public function get_props(): array {
return [];
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls\Types\Elements;
use Elementor\Modules\AtomicWidgets\Controls\Base\Element_Control_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Tabs_Control extends Element_Control_Base {
public function get_type(): string {
return 'tabs';
}
public function get_props(): array {
return [];
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls\Types;
use Elementor\Modules\AtomicWidgets\Controls\Base\Atomic_Control_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Html_Tag_Control extends Select_Control {
public function get_type(): string {
return 'html-tag';
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls\Types;
use Elementor\Modules\AtomicWidgets\Controls\Base\Atomic_Control_Base;
use Elementor\Modules\AtomicWidgets\Utils\Image\Image_Sizes;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Image_Control extends Atomic_Control_Base {
public function get_type(): string {
return 'image';
}
public function get_props(): array {
return [
'sizes' => Image_Sizes::get_all(),
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls\Types;
use Elementor\Modules\AtomicWidgets\Controls\Base\Atomic_Control_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Inline_Editing_Control extends Atomic_Control_Base {
private ?string $placeholder = null;
public function get_type(): string {
return 'inline-editing';
}
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,50 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls\Types;
use Elementor\Modules\AtomicWidgets\Controls\Base\Atomic_Control_Base;
use Elementor\Modules\AtomicWidgets\Query\Query_Builder_Factory as Query_Builder;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Link_Control extends Atomic_Control_Base {
private bool $allow_custom_values = true;
private int $minimum_input_length = 2;
private ?array $query_config = null;
private ?string $placeholder = null;
private ?string $aria_label = null;
public function get_type(): string {
return 'link';
}
public function set_placeholder( string $placeholder ): self {
$this->placeholder = $placeholder;
return $this;
}
public function set_allow_custom_values( bool $allow_custom_values ): self {
$this->allow_custom_values = $allow_custom_values;
return $this;
}
public function set_query_config( $config ): self {
$this->query_config = $config;
return $this;
}
public function get_props(): array {
return [
'allowCustomValues' => $this->allow_custom_values,
'placeholder' => $this->placeholder,
'queryOptions' => Query_Builder::create( $this->query_config )->build(),
'minInputLength' => $this->minimum_input_length,
'ariaLabel' => 'Link URL',
];
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls\Types;
use Elementor\Modules\AtomicWidgets\Controls\Base\Atomic_Control_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Number_Control extends Atomic_Control_Base {
private ?string $placeholder = null;
private ?int $max = null;
private ?int $min = null;
private ?int $step = null;
private ?bool $should_force_int = null;
public function get_type(): string {
return 'number';
}
public function set_placeholder( string $placeholder ): self {
$this->placeholder = $placeholder;
return $this;
}
public function set_max( ?int $max ): self {
$this->max = $max;
return $this;
}
public function set_min( ?int $min ): self {
$this->min = $min;
return $this;
}
public function set_step( ?int $step ): self {
$this->step = $step;
return $this;
}
public function set_should_force_int( ?bool $should_force_int ): self {
$this->should_force_int = $should_force_int ?? false;
return $this;
}
public function get_props(): array {
return [
'placeholder' => $this->placeholder,
'max' => $this->max,
'min' => $this->min,
'step' => $this->step,
'shouldForceInt' => $this->should_force_int,
];
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls\Types;
use Elementor\Modules\AtomicWidgets\Controls\Base\Atomic_Control_Base;
use Elementor\Modules\AtomicWidgets\Query\Query_Builder_Factory as Query_Builder;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Query_Control extends Atomic_Control_Base {
private bool $allow_custom_values = true;
private int $minimum_input_length = 2;
private ?array $query_config = null;
private ?string $placeholder = null;
public function get_type(): string {
return 'query';
}
public function set_placeholder( string $placeholder ): self {
$this->placeholder = $placeholder;
return $this;
}
public function set_allow_custom_values( bool $allow_custom_values ): self {
$this->allow_custom_values = $allow_custom_values;
return $this;
}
public function set_query_config( $config ): self {
$this->query_config = $config;
return $this;
}
public function get_props(): array {
return [
'allowCustomValues' => $this->allow_custom_values,
'placeholder' => $this->placeholder,
'queryOptions' => Query_Builder::create( $this->query_config )->build(),
'minInputLength' => $this->minimum_input_length,
];
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls\Types;
use Elementor\Modules\AtomicWidgets\Controls\Base\Atomic_Control_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Repeatable_Control extends Atomic_Control_Base {
private string $child_control_type;
private object $child_control_props;
private bool $show_duplicate = true;
private bool $show_toggle = true;
private string $repeater_label;
private ?object $initial_values;
private ?string $pattern_label;
private ?string $placeholder;
private ?string $prop_key = '';
public function get_type(): string {
return 'repeatable';
}
public function set_child_control_type( $control_type ): self {
$this->child_control_type = $control_type;
return $this;
}
public function set_child_control_props( $control_props ): self {
$this->child_control_props = (object) $control_props;
return $this;
}
public function hide_duplicate(): self {
$this->show_duplicate = false;
return $this;
}
public function hide_toggle(): self {
$this->show_toggle = false;
return $this;
}
public function set_initialValues( $initial_values ): self {
$this->initial_values = (object) $initial_values;
return $this;
}
public function set_patternLabel( $pattern_label ): self {
$this->pattern_label = $pattern_label;
return $this;
}
public function set_repeaterLabel( string $label ): self {
$this->repeater_label = $label;
return $this;
}
public function set_placeholder( string $placeholder ): self {
$this->placeholder = $placeholder;
return $this;
}
public function set_prop_key( string $prop_key ): self {
$this->prop_key = $prop_key;
return $this;
}
public function get_props(): array {
return [
'childControlType' => $this->child_control_type,
'childControlProps' => $this->child_control_props,
'showDuplicate' => $this->show_duplicate,
'showToggle' => $this->show_toggle,
'initialValues' => $this->initial_values,
'patternLabel' => $this->pattern_label,
'repeaterLabel' => $this->repeater_label,
'placeholder' => $this->placeholder,
'propKey' => $this->prop_key,
];
}
}

View File

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

View File

@@ -0,0 +1,60 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls\Types;
use Elementor\Modules\AtomicWidgets\Controls\Base\Atomic_Control_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Size_Control extends Atomic_Control_Base {
private ?string $placeholder = null;
private ?string $variant = 'length';
private ?array $units = null;
private ?string $default_unit = null;
private ?bool $disable_custom = false;
public function get_type(): string {
return 'size';
}
public function set_placeholder( string $placeholder ): self {
$this->placeholder = $placeholder;
return $this;
}
public function set_variant( string $variant ): self {
$this->variant = $variant;
return $this;
}
public function set_units( array $units ): self {
$this->units = $units;
return $this;
}
public function set_default_unit( string $default_unit ): self {
$this->default_unit = $default_unit;
return $this;
}
public function set_disable_custom( bool $disable_custom ): self {
$this->disable_custom = $disable_custom;
return $this;
}
public function get_props(): array {
return [
'placeholder' => $this->placeholder,
'variant' => $this->variant,
'units' => $this->units,
'defaultUnit' => $this->default_unit,
'disableCustom' => $this->disable_custom,
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls\Types;
use Elementor\Modules\AtomicWidgets\Controls\Base\Atomic_Control_Base;
use Elementor\Modules\AtomicWidgets\Utils\Image\Image_Sizes;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Svg_Control extends Atomic_Control_Base {
public function get_type(): string {
return 'svg-media';
}
public function get_props(): array {
return [
'type' => $this->get_type(),
];
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls\Types;
use Elementor\Modules\AtomicWidgets\Controls\Base\Atomic_Control_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Switch_Control extends Atomic_Control_Base {
public function get_type(): string {
return 'switch';
}
public function get_props(): array {
return [];
}
}

View File

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

View File

@@ -0,0 +1,74 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls\Types;
use Elementor\Modules\AtomicWidgets\Controls\Base\Atomic_Control_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Toggle_Control extends Atomic_Control_Base {
private array $options = [];
private bool $full_width = false;
private string $size = 'tiny';
private bool $exclusive = true;
private bool $convert_options = false;
public function get_type(): string {
return 'toggle';
}
public function add_options( array $control_options ): self {
$this->options = [];
foreach ( $control_options as $value => $config ) {
$this->options[] = [
'value' => $value,
'label' => $config['title'] ?? $value,
'icon' => $config['atomic-icon'] ?? null,
'showTooltip' => true,
'exclusive' => false,
];
}
return $this;
}
public function set_size( string $size ): self {
$allowed_sizes = [ 'tiny', 'small', 'medium', 'large' ];
if ( in_array( $size, $allowed_sizes, true ) ) {
$this->size = $size;
}
return $this;
}
public function set_exclusive( bool $exclusive ): self {
$this->exclusive = $exclusive;
return $this;
}
/**
* Whether to convert the v3 options to v4 compatible
*
* @param bool $convert_options
* @return $this
*/
public function set_convert_options( bool $convert_options ): self {
$this->convert_options = $convert_options;
return $this;
}
public function get_props(): array {
return [
'options' => $this->options,
'fullWidth' => $this->full_width,
'size' => $this->size,
'exclusive' => $this->exclusive,
'convertOptions' => $this->convert_options,
];
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Database;
use Elementor\Core\Database\Base_Database_Updater;
use Elementor\Modules\AtomicWidgets\Database\Migrations\Add_Capabilities;
class Atomic_Widgets_Database_Updater extends Base_Database_Updater {
const DB_VERSION = 1;
const OPTION_NAME = 'elementor_atomic_widgets_db_version';
protected function get_migrations(): array {
return [
1 => new Add_Capabilities(),
];
}
protected function get_db_version() {
return static::DB_VERSION;
}
protected function get_db_version_option_name(): string {
return static::OPTION_NAME;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Database\Migrations;
use Elementor\Core\Database\Base_Migration;
class Add_Capabilities extends Base_Migration {
const ACCESS_STYLES_TAB = 'elementor_atomic_widgets_access_styles_tab';
const EDIT_LOCAL_CSS_CLASS = 'elementor_atomic_widgets_edit_local_css_class';
public function up() {
$capabilities = [
self::ACCESS_STYLES_TAB => [ 'administrator', 'editor', 'author', 'contributor', 'shop_manager' ],
self::EDIT_LOCAL_CSS_CLASS => [ 'administrator', 'editor', 'author', 'contributor', 'shop_manager' ],
];
foreach ( $capabilities as $cap => $roles ) {
foreach ( $roles as $role_name ) {
$role = get_role( $role_name );
if ( $role ) {
$role->add_cap( $cap );
}
}
}
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace Elementor\Modules\AtomicWidgets\DynamicTags;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Plain_Prop_Type;
use Elementor\Modules\AtomicWidgets\Parsers\Props_Parser;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Dynamic_Prop_Type extends Plain_Prop_Type {
const META_KEY = 'dynamic';
/**
* Return a tuple that lets the developer ignore the dynamic prop type in the props schema
* using `Prop_Type::meta()`, e.g. `String_Prop_Type::make()->meta( Dynamic_Prop_Type::ignore() )`.
*/
public static function ignore(): array {
return [ static::META_KEY, false ];
}
public static function get_key(): string {
return 'dynamic';
}
public function categories( array $categories ) {
$this->settings['categories'] = $categories;
return $this;
}
public function get_categories() {
return $this->settings['categories'] ?? [];
}
public static function is_dynamic_prop_value( $value ): bool {
return isset( $value['$$type'] ) && self::get_key() === $value['$$type'];
}
protected function validate_value( $value ): bool {
$is_valid_structure = (
isset( $value['name'] ) &&
is_string( $value['name'] ) &&
isset( $value['group'] ) &&
is_string( $value['group'] ) &&
isset( $value['settings'] ) &&
is_array( $value['settings'] )
);
if ( ! $is_valid_structure ) {
return false;
}
$tag = Dynamic_Tags_Module::instance()->registry->get_tag( $value['name'] );
if ( ! $tag || ! $this->is_tag_in_supported_categories( $tag ) ) {
return false;
}
return Props_Parser::make( $tag['props_schema'] )
->validate( $value['settings'] )
->is_valid();
}
protected function sanitize_value( $value ): array {
$tag = Dynamic_Tags_Module::instance()->registry->get_tag( $value['name'] );
$sanitized = Props_Parser::make( $tag['props_schema'] )
->sanitize( $value['settings'] )
->unwrap();
return [
'name' => $value['name'],
'group' => $value['group'],
'settings' => $sanitized,
];
}
private function is_tag_in_supported_categories( array $tag ): bool {
$intersection = array_intersect(
$tag['categories'],
$this->get_categories()
);
return ! empty( $intersection );
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace Elementor\Modules\AtomicWidgets\DynamicTags;
use Elementor\Modules\AtomicWidgets\PropTypes\Utils\Prop_Types_Schema_Extender;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Transformable_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Html_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Image_Src_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\Number_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Color_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Url_Prop_Type;
use Elementor\Modules\DynamicTags\Module as V1_Dynamic_Tags_Module;
use Elementor\Modules\AtomicWidgets\PropTypes\Union_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Dynamic_Prop_Types_Mapping extends Prop_Types_Schema_Extender {
public static function make(): self {
return new static();
}
/**
* Get the dynamic prop type to add to the prop type
*
* @param Prop_Type $prop_type
*/
protected function get_prop_types_to_add( Prop_Type $prop_type ): array {
$categories = [];
$transformable_prop_types = $prop_type instanceof Union_Prop_Type ?
$prop_type->get_prop_types() :
[ $prop_type ];
foreach ( $transformable_prop_types as $transformable_prop_type ) {
if ( $transformable_prop_type instanceof Transformable_Prop_Type ) {
// When the prop type is originally a union, we need to merge all the categories
// of each prop type in the union and create one dynamic prop type with all the categories.
$categories = array_merge( $categories, $this->get_related_categories( $transformable_prop_type ) );
}
}
if ( empty( $categories ) ) {
return [];
}
return [ Dynamic_Prop_Type::make()->categories( $categories ) ];
}
private function get_related_categories( Transformable_Prop_Type $prop_type ): array {
if ( ! $prop_type->get_meta_item( Dynamic_Prop_Type::META_KEY, true ) ) {
return [];
}
if ( $prop_type instanceof Number_Prop_Type ) {
return [ V1_Dynamic_Tags_Module::NUMBER_CATEGORY ];
}
if ( $prop_type->get_meta_item( 'is_svg', false ) ) {
return [ V1_Dynamic_Tags_Module::SVG_CATEGORY ];
}
if ( $prop_type instanceof Image_Src_Prop_Type ) {
return [ V1_Dynamic_Tags_Module::IMAGE_CATEGORY ];
}
if ( $prop_type instanceof Url_Prop_Type ) {
return [ V1_Dynamic_Tags_Module::URL_CATEGORY ];
}
if ( $prop_type instanceof Html_Prop_Type ) {
return [ V1_Dynamic_Tags_Module::TEXT_CATEGORY ];
}
if ( $prop_type instanceof Color_Prop_Type ) {
return [ V1_Dynamic_Tags_Module::COLOR_CATEGORY ];
}
if ( $prop_type instanceof String_Prop_Type && empty( $prop_type->get_enum() ) ) {
return [ V1_Dynamic_Tags_Module::TEXT_CATEGORY ];
}
return [];
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace Elementor\Modules\AtomicWidgets\DynamicTags;
use Elementor\Modules\AtomicWidgets\Utils\Image\Placeholder_Image;
use Elementor\Modules\AtomicWidgets\PropDependencies\Manager as Dependency_Manager;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Object_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Plain_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Date_Time_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Image_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\Boolean_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\Number_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Query_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Dynamic_Tags_Converter {
/**
* @param array $control
* @return Plain_Prop_Type|Object_Prop_Type|null
*/
public static function convert_control_to_prop_type( array $control ) {
$control_type = $control['type'];
switch ( $control_type ) {
case 'text':
case 'textarea':
$prop_type = String_Prop_Type::make()
->default( $control['default'] ?? null );
break;
case 'select':
$prop_type = String_Prop_Type::make()
->default( $control['default'] ?? null );
if ( ! isset( $control['collection_id'] ) || empty( $control['collection_id'] ) ) {
$prop_type->enum( array_keys( $control['options'] ?? [] ) );
}
break;
case 'date_time':
$prop_type = Date_Time_Prop_Type::make()
->default( $control['default'] ?? null );
break;
case 'number':
$prop_type = Number_Prop_Type::make()
->set_required( $control['required'] ?? false )
->default( $control['default'] ?? null );
break;
case 'switcher':
$default = $control['default'];
$prop_type = Boolean_Prop_Type::make()
->default( 'yes' === $default || true === $default );
break;
case 'choose':
$prop_type = String_Prop_Type::make()
->default( $control['default'] ?? null )
->enum( array_keys( $control['options'] ?? [] ) );
break;
case 'query':
$prop_type = Query_Prop_Type::make()
->set_required( $control['required'] ?? false )
->default( $control['default'] ?? null );
break;
case 'media':
$prop_type = Image_Prop_Type::make()
->default_url( Placeholder_Image::get_placeholder_image() )
->default_size( 'full' )
->set_shape_meta( 'src', [ 'isDynamic' => true ] );
break;
default:
return null;
}
$prop_type->set_dependencies( self::create_dependencies_from_condition( $control['condition'] ?? null ) );
return $prop_type;
}
private static function create_dependencies_from_condition( $condition ): ?array {
if ( ! is_array( $condition ) || empty( $condition ) ) {
return null;
}
$manager = Dependency_Manager::make( Dependency_Manager::RELATION_AND );
foreach ( $condition as $raw_key => $value ) {
$is_negated = false !== strpos( (string) $raw_key, '!' );
$key = rtrim( (string) $raw_key, '!' );
$path = self::parse_condition_path( $key );
if ( is_array( $value ) ) {
$manager->where( [
'operator' => $is_negated ? 'nin' : 'in',
'path' => $path,
'value' => $value,
] );
continue;
}
$manager->where( [
'operator' => $is_negated ? 'ne' : 'eq',
'path' => $path,
'value' => $value,
] );
}
return $manager->get();
}
private static function parse_condition_path( string $key ): array {
if ( false === strpos( $key, '[' ) ) {
return [ $key ];
}
$key = str_replace( ']', '', $key );
$tokens = explode( '[', $key );
return array_values( array_filter( $tokens, static fn( $t ) => '' !== $t ) );
}
}

View File

@@ -0,0 +1,389 @@
<?php
namespace Elementor\Modules\AtomicWidgets\DynamicTags;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Modules\AtomicWidgets\Controls\Types\Date_Time_Control;
use Elementor\Modules\AtomicWidgets\Controls\Types\Image_Control;
use Elementor\Modules\AtomicWidgets\Controls\Types\Toggle_Control;
use Elementor\Modules\AtomicWidgets\Controls\Types\Query_Control;
use Elementor\Modules\AtomicWidgets\Controls\Types\Select_Control;
use Elementor\Modules\AtomicWidgets\Controls\Types\Text_Control;
use Elementor\Modules\AtomicWidgets\Controls\Types\Switch_Control;
use Elementor\Modules\AtomicWidgets\Controls\Types\Number_Control;
use Elementor\Modules\AtomicWidgets\Controls\Types\Textarea_Control;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Transformable_Prop_Type;
use Elementor\Modules\AtomicWidgets\Query\Query_Builder;
use Elementor\Modules\AtomicWidgets\Query\Query_Builder_Factory;
use Elementor\Modules\WpRest\Base\Query as Query_Base;
use Elementor\Modules\WpRest\Classes\Post_Query;
use Elementor\Modules\WpRest\Classes\Term_Query;
use Elementor\Modules\WpRest\Classes\User_Query;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Dynamic_Tags_Editor_Config {
private Dynamic_Tags_Schemas $schemas;
private ?array $tags = null;
public function __construct( Dynamic_Tags_Schemas $schemas ) {
$this->schemas = $schemas;
}
public function get_tags(): array {
if ( null !== $this->tags ) {
return $this->tags;
}
$atomic_tags = [];
$dynamic_tags = Plugin::$instance->dynamic_tags->get_tags_config();
foreach ( $dynamic_tags as $name => $tag ) {
$atomic_tag = $this->convert_dynamic_tag_to_atomic( $tag );
if ( $atomic_tag ) {
$atomic_tags[ $name ] = $atomic_tag;
}
}
$this->tags = $atomic_tags;
return $this->tags;
}
/**
* @param string $name
*
* @return null|array{
* name: string,
* categories: string[],
* label: string,
* group: string,
* atomic_controls: array,
* props_schema: array<string, Transformable_Prop_Type>
* }
*/
public function get_tag( string $name ): ?array {
$tags = $this->get_tags();
return $tags[ $name ] ?? null;
}
private function convert_dynamic_tag_to_atomic( $tag ) {
if ( empty( $tag['name'] ) || empty( $tag['categories'] ) ) {
return null;
}
$converted_tag = [
'name' => $tag['name'],
'categories' => $tag['categories'],
'label' => $tag['title'] ?? '',
'group' => $tag['atomic_group'] ?? $tag['group'] ?? '',
'atomic_controls' => [],
'props_schema' => $this->schemas->get( $tag['name'] ),
];
if ( ! isset( $tag['controls'] ) ) {
return $converted_tag;
}
try {
$atomic_controls = $this->convert_controls_to_atomic( $tag );
} catch ( \Exception $e ) {
return null;
}
if ( null === $atomic_controls ) {
return null;
}
$converted_tag['atomic_controls'] = $atomic_controls;
return $converted_tag;
}
private function convert_controls_to_atomic( $tag ) {
$atomic_controls = [];
$controls = $tag['controls'] ?? null;
$force = $tag['force_convert_to_atomic'] ?? false;
if ( ! is_array( $controls ) ) {
return null;
}
foreach ( $controls as $control ) {
if ( 'section' === $control['type'] ) {
continue;
}
$atomic_control = $this->convert_control_to_atomic( $control, $tag );
if ( ! $atomic_control ) {
if ( $force ) {
continue;
}
return null;
}
$section_name = $control['section'];
if ( ! isset( $atomic_controls[ $section_name ] ) ) {
$atomic_controls[ $section_name ] = Section::make()
->set_label( $controls[ $section_name ]['label'] );
}
$atomic_controls[ $section_name ] = $atomic_controls[ $section_name ]->add_item( $atomic_control );
}
return array_values( $atomic_controls );
}
private function convert_control_to_atomic( $control, $tag = [] ) {
$map = [
'select' => fn( $control ) => $this->convert_select_control_to_atomic( $control, $tag ),
'text' => fn( $control ) => $this->convert_text_control_to_atomic( $control ),
'textarea' => fn( $control ) => $this->convert_textarea_control_to_atomic( $control ),
'switcher' => fn( $control ) => $this->convert_switch_control_to_atomic( $control ),
'number' => fn( $control ) => $this->convert_number_control_to_atomic( $control ),
'query' => fn( $control ) => $this->convert_autocomplete_control_to_atomic( $control ),
'choose' => fn( $control ) => $this->convert_choose_control_to_atomic( $control ),
'media' => fn( $control ) => $this->convert_media_control_to_atomic( $control ),
'date_time' => fn( $control ) => $this->convert_date_time_control_to_atomic( $control ),
];
if ( ! isset( $map[ $control['type'] ] ) ) {
return null;
}
$is_convertable = ! isset( $control['name'], $control['section'], $control['label'], $control['default'] );
if ( $is_convertable ) {
throw new \Exception( 'Control must have name, section, label, and default' );
}
return $map[ $control['type'] ]( $control );
}
/**
* @param $control
*
* @return Select_Control
* @throws \Exception If control is missing options.
*/
private function convert_select_control_to_atomic( $control, $tag = [] ) {
$options = $this->extract_select_options_from_control( $control );
if ( empty( $options ) ) {
throw new \Exception( 'Select control must have options' );
}
$options = apply_filters( 'elementor/atomic/dynamic_tags/select_control_options', $options, $control, $tag );
$options = array_map(
fn( $key, $value ) => [
'value' => $key,
'label' => $value,
],
array_keys( $options ),
$options
);
$select_control = Select_Control::bind_to( $control['name'] )
->set_placeholder( $control['placeholder'] ?? '' )
->set_options( $options )
->set_label( $control['atomic_label'] ?? $control['label'] );
if ( isset( $control['collection_id'] ) ) {
$select_control->set_collection_id( $control['collection_id'] );
}
return $select_control;
}
private function extract_select_options_from_control( $control ): array {
$options = $control['options'] ?? [];
if ( ! empty( $options ) ) {
return $options;
}
if ( empty( $control['groups'] ) || ! is_array( $control['groups'] ) ) {
return $options;
}
foreach ( $control['groups'] as $group ) {
if ( empty( $group['options'] ) || ! is_array( $group['options'] ) ) {
continue;
}
$filtered = array_filter(
$group['options'],
static function ( $label, $key ) {
return is_string( $key );
},
ARRAY_FILTER_USE_BOTH
);
$options = array_merge( $options, $filtered );
}
return $options;
}
/**
* @param $control
*
* @return Text_Control
*/
private function convert_text_control_to_atomic( $control ) {
return Text_Control::bind_to( $control['name'] )
->set_label( $control['label'] );
}
private function convert_date_time_control_to_atomic( $control ) {
return Date_Time_Control::bind_to( $control['name'] )
->set_label( $control['label'] );
}
/**
* @param $control
*
* @return Switch_Control
*/
private function convert_switch_control_to_atomic( $control ) {
return Switch_Control::bind_to( $control['name'] )
->set_label( $control['atomic_label'] ?? $control['label'] );
}
/**
* @param $control
*
* @return Number_Control
*/
private function convert_number_control_to_atomic( $control ) {
return Number_Control::bind_to( $control['name'] )
->set_placeholder( $control['placeholder'] ?? '' )
->set_max( $control['max'] ?? null )
->set_min( $control['min'] ?? null )
->set_step( $control['step'] ?? null )
->set_should_force_int( $control['should_force_int'] ?? false )
->set_label( $control['label'] );
}
private function convert_textarea_control_to_atomic( $control ) {
return Textarea_Control::bind_to( $control['name'] )
->set_placeholder( $control['placeholder'] ?? '' )
->set_label( $control['label'] );
}
private function convert_autocomplete_control_to_atomic( $control ) {
$query_config = [];
$query_type = Post_Query::ENDPOINT;
switch ( true ) {
case $this->is_querying_wp_terms( $control ):
$query_type = Term_Query::ENDPOINT;
$included_types = null;
$excluded_types = null;
break;
case $this->is_control_elementor_query( $control ):
$included_types = [ Source_Local::CPT ];
$excluded_types = [];
break;
case $this->is_querying_wp_media( $control ):
$included_types = [ 'attachment' ];
$excluded_types = [];
$query_config[ Query_Base::IS_PUBLIC_KEY ] = false;
break;
case $this->is_querying_wp_users( $control ):
$included_types = [ $control['autocomplete']['object'] ];
$excluded_types = null;
$query_type = User_Query::ENDPOINT;
break;
default:
$included_types = isset( $control['autocomplete']['query']['post_type'] ) ? $control['autocomplete']['query']['post_type'] : [];
$included_types = ! empty( $included_types ) && 'any' !== $included_types ? $included_types : null;
$excluded_types = null;
}
$query_config[ Query_Base::ITEMS_COUNT_KEY ] = $this->extract_item_count_from_control( $control );
$post_status[ Query_Base::IS_PUBLIC_KEY ] = $this->extract_post_status_from_control( $control );
$query_config[ Query_Base::INCLUDED_TYPE_KEY ] = $included_types;
$query_config[ Query_Base::EXCLUDED_TYPE_KEY ] = $excluded_types;
$query_config[ Query_Builder_Factory::ENDPOINT_KEY ] = $query_type;
$query_config[ Query_Base::META_QUERY_KEY ] = $this->extract_meta_query_from_control( $control );
$query_control = Query_Control::bind_to( $control['name'] );
$query_control->set_query_config( $query_config );
$query_control->set_placeholder( $control['placeholder'] ?? '' );
$query_control->set_label( $control['label'] );
$query_control->set_allow_custom_values( false );
return $query_control;
}
private function is_control_elementor_query( $control ): bool {
return isset( $control['autocomplete']['object'] ) && 'library_template' === $control['autocomplete']['object'];
}
private function is_querying_wp_terms( $control ): bool {
return isset( $control['autocomplete']['object'] ) && in_array( $control['autocomplete']['object'], [ 'tax', 'taxonomy', 'term' ], true );
}
private function is_querying_wp_media( $control ): bool {
return isset( $control['autocomplete']['object'] ) && 'attachment' === $control['autocomplete']['object'];
}
private function is_querying_wp_users( $control ): bool {
global $wp_roles;
$roles = array_keys( $wp_roles->roles );
return isset( $control['autocomplete']['object'] ) && in_array( $control['autocomplete']['object'], $roles, true );
}
private function convert_choose_control_to_atomic( $control ) {
return Toggle_Control::bind_to( $control['name'] )
->set_label( $control['atomic_label'] ?? $control['label'] )
->add_options( $control['options'] )
->set_size( 'tiny' )
->set_exclusive( true )
->set_convert_options( true );
}
private function convert_media_control_to_atomic( $control ) {
return Image_Control::bind_to( $control['name'] )
->set_label( $control['label'] );
}
private function extract_post_status_from_control( $control ): ?bool {
$status = $control['autocomplete']['query']['post_status'] ?? null;
return isset( $status ) && in_array( 'private', $status )
? false
: null;
}
private function extract_item_count_from_control( $control ): ?int {
$count = $control['autocomplete']['query']['posts_per_page'] ?? null;
return isset( $count ) && is_numeric( $count )
? $count
: null;
}
private function extract_meta_query_from_control( $control ): ?array {
return $control['autocomplete']['query']['meta_query'] ?? null;
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace Elementor\Modules\AtomicWidgets\DynamicTags;
use Elementor\Modules\AtomicWidgets\DynamicTags\ImportExport\Dynamic_Transformer as Import_Export_Dynamic_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Render_Props_Resolver;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers_Registry;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Dynamic_Tags_Module {
private static ?self $instance = null;
public Dynamic_Tags_Editor_Config $registry;
private Dynamic_Tags_Schemas $schemas;
private function __construct() {
$this->schemas = new Dynamic_Tags_Schemas();
$this->registry = new Dynamic_Tags_Editor_Config( $this->schemas );
}
public static function instance( $fresh = false ): self {
if ( null === static::$instance || $fresh ) {
static::$instance = new static();
}
return static::$instance;
}
public static function fresh(): self {
return static::instance( true );
}
public function register_hooks() {
add_filter(
'elementor/editor/localize_settings',
fn( array $settings ) => $this->add_atomic_dynamic_tags_to_editor_settings( $settings )
);
add_filter(
'elementor/atomic-widgets/props-schema',
fn( array $schema ) => Dynamic_Prop_Types_Mapping::make()->get_extended_schema( $schema )
);
add_filter(
'elementor/atomic-widgets/styles/schema',
fn( array $schema ) => Dynamic_Prop_Types_Mapping::make()->get_extended_style_schema( $schema ),
8,
2
);
add_action(
'elementor/atomic-widgets/settings/transformers/register',
fn ( $transformers, $prop_resolver ) => $this->register_transformers( $transformers, $prop_resolver ),
10,
2
);
add_action(
'elementor/atomic-widgets/styles/transformers/register',
fn ( $transformers, $prop_resolver ) => $this->register_transformers( $transformers, $prop_resolver ),
10,
2
);
add_action(
'elementor/atomic-widgets/import/transformers/register',
fn ( $transformers ) => $this->register_import_export_transformer( $transformers )
);
add_action(
'elementor/atomic-widgets/export/transformers/register',
fn ( $transformers ) => $this->register_import_export_transformer( $transformers )
);
}
private function add_atomic_dynamic_tags_to_editor_settings( $settings ) {
if ( isset( $settings['dynamicTags']['tags'] ) ) {
$settings['atomicDynamicTags'] = [
'tags' => $this->registry->get_tags(),
'groups' => Plugin::$instance->dynamic_tags->get_config()['groups'],
];
}
return $settings;
}
private function register_transformers( Transformers_Registry $transformers, Render_Props_Resolver $props_resolver ) {
$transformers->register(
Dynamic_Prop_Type::get_key(),
new Dynamic_Transformer(
Plugin::$instance->dynamic_tags,
$this->schemas,
$props_resolver
)
);
}
private function register_import_export_transformer( Transformers_Registry $transformers ) {
$transformers->register(
Dynamic_Prop_Type::get_key(),
new Import_Export_Dynamic_Transformer()
);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Elementor\Modules\AtomicWidgets\DynamicTags;
use Elementor\Core\DynamicTags\Base_Tag;
use Elementor\Modules\AtomicWidgets\Utils\Image\Placeholder_Image;
use Elementor\Modules\AtomicWidgets\PropTypes\Image_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropDependencies\Manager as Dependency_Manager;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\Boolean_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\Number_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Query_Prop_Type;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Dynamic_Tags_Schemas {
private array $tags_schemas = [];
public function get( string $tag_name ) {
if ( isset( $this->tags_schemas[ $tag_name ] ) ) {
return $this->tags_schemas[ $tag_name ];
}
$tag = $this->get_tag( $tag_name );
$this->tags_schemas[ $tag_name ] = [];
foreach ( $tag->get_controls() as $control ) {
if ( ! isset( $control['type'] ) || 'section' === $control['type'] ) {
continue;
}
$prop_type = Dynamic_Tags_Converter::convert_control_to_prop_type( $control );
if ( ! $prop_type ) {
continue;
}
$this->tags_schemas[ $tag_name ][ $control['name'] ] = $prop_type;
}
return $this->tags_schemas[ $tag_name ];
}
private function get_tag( string $tag_name ): Base_Tag {
$tag_info = Plugin::$instance->dynamic_tags->get_tag_info( $tag_name );
if ( ! $tag_info || empty( $tag_info['instance'] ) ) {
throw new \Exception( 'Tag not found' );
}
if ( ! $tag_info['instance'] instanceof Base_Tag ) {
throw new \Exception( 'Tag is not an instance of Tag' );
}
return $tag_info['instance'];
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Elementor\Modules\AtomicWidgets\DynamicTags;
use Elementor\Core\DynamicTags\Manager as Dynamic_Tags_Manager;
use Elementor\Modules\AtomicWidgets\PropsResolver\Render_Props_Resolver;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformer_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Dynamic_Transformer extends Transformer_Base {
private Dynamic_Tags_Manager $dynamic_tags_manager;
private Dynamic_Tags_Schemas $dynamic_tags_schemas;
private Render_Props_Resolver $props_resolver;
public function __construct(
Dynamic_Tags_Manager $dynamic_tags_manager,
Dynamic_Tags_Schemas $dynamic_tags_schemas,
Render_Props_Resolver $props_resolver
) {
$this->dynamic_tags_manager = $dynamic_tags_manager;
$this->dynamic_tags_schemas = $dynamic_tags_schemas;
$this->props_resolver = $props_resolver;
}
public function transform( $value, $key ) {
if ( ! isset( $value['name'] ) || ! is_string( $value['name'] ) ) {
throw new \Exception( 'Dynamic tag name must be a string' );
}
if ( isset( $value['settings'] ) && ! is_array( $value['settings'] ) ) {
throw new \Exception( 'Dynamic tag settings must be an array' );
}
$schema = $this->dynamic_tags_schemas->get( $value['name'] );
$settings = $this->props_resolver->resolve(
$schema,
$value['settings'] ?? []
);
return $this->dynamic_tags_manager->get_tag_data_content( null, $value['name'], $settings );
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Elementor\Modules\AtomicWidgets\DynamicTags\ImportExport;
use Elementor\Modules\AtomicWidgets\DynamicTags\Dynamic_Prop_Type;
use Elementor\Modules\AtomicWidgets\DynamicTags\Dynamic_Tags_Module;
use Elementor\Modules\AtomicWidgets\PropsResolver\Props_Resolver_Context;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformer_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Dynamic_Transformer extends Transformer_Base {
public function transform( $value, Props_Resolver_Context $context ): ?array {
if ( empty( $value['name'] ) || ! is_string( $value['name'] ) ) {
return null;
}
$tag = Dynamic_Tags_Module::instance()->registry->get_tag( $value['name'] );
if ( ! $tag ) {
return null;
}
$group = $value['group'] ?? $tag['group'] ?? '';
return Dynamic_Prop_Type::generate( [
'name' => $value['name'],
'group' => $group,
'settings' => $value['settings'] ?? [],
], $context->is_disabled() );
}
}

View File

@@ -0,0 +1,21 @@
{% if settings.text is not empty %}
{% set classes = settings.classes | merge( [ base_styles.base ] ) | join(' ') %}
{% set id_attribute = settings._cssid is not empty ? 'id=' ~ settings._cssid | e('html_attr') : '' %}
{% if settings.link.href %}
<{{ settings.link.tag | e('html_tag') }}
{% set linkAttr = settings.link.tag == 'a' ? 'href' : 'data-action-link' %}
{{ linkAttr }}="{{ settings.link.href | raw }}"
target="{{ settings.link.target }}"
class="{{ classes }}"
data-interaction-id="{{ id }}"
data-interactions="{{ interactions | json_encode | e('html_attr') }}"
{{ id_attribute }} {{ settings.attributes | raw }}
>
{{ settings.text }}
</{{ settings.link.tag | e('html_tag') }}>
{% else %}
<button class="{{ classes }}" data-interaction-id="{{ id }}" data-interactions="{{ interactions | json_encode | e('html_attr') }}" {{ id_attribute }} {{ settings.attributes | raw }}>
{{ settings.text }}
</button>
{% endif %}
{% endif %}

View File

@@ -0,0 +1,141 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Atomic_Button;
use Elementor\Modules\AtomicWidgets\Controls\Types\Text_Control;
use Elementor\Modules\AtomicWidgets\Elements\Base\Atomic_Widget_Base;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Modules\AtomicWidgets\Controls\Types\Link_Control;
use Elementor\Modules\AtomicWidgets\Elements\Base\Has_Template;
use Elementor\Modules\AtomicWidgets\PropTypes\Background_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Attributes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Classes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Color_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Link_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Dimensions_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Size_Prop_Type;
use Elementor\Modules\AtomicWidgets\Styles\Style_Definition;
use Elementor\Modules\AtomicWidgets\Styles\Style_Variant;
use Elementor\Modules\Components\PropTypes\Overridable_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Atomic_Button extends Atomic_Widget_Base {
use Has_Template;
public static function get_element_type(): string {
return 'e-button';
}
public function get_title() {
return esc_html__( 'Button', 'elementor' );
}
public function get_keywords() {
return [ 'ato', 'atom', 'atoms', 'atomic' ];
}
public function get_icon() {
return 'eicon-e-button';
}
protected static function define_props_schema(): array {
$props = [
'classes' => Classes_Prop_Type::make()
->default( [] ),
'text' => String_Prop_Type::make()
->default( __( 'Click here', 'elementor' ) )
->description( 'The text displayed on the button.' ),
'link' => Link_Prop_Type::make(),
'attributes' => Attributes_Prop_Type::make()->meta( Overridable_Prop_Type::ignore() ),
];
return $props;
}
protected function define_atomic_controls(): array {
return [
Section::make()
->set_label( __( 'Content', 'elementor' ) )
->set_items( [
Text_Control::bind_to( 'text' )
->set_placeholder( __( 'Type your button text here', 'elementor' ) )
->set_label( __( 'Button text', 'elementor' ) ),
] ),
Section::make()
->set_label( __( 'Settings', 'elementor' ) )
->set_id( 'settings' )
->set_items( $this->get_settings_controls() ),
];
}
protected function get_settings_controls(): array {
return [
Link_Control::bind_to( 'link' )
->set_placeholder( __( 'Type or paste your URL', 'elementor' ) )
->set_label( __( 'Link', 'elementor' ) ),
Text_Control::bind_to( '_cssid' )
->set_label( __( 'ID', 'elementor' ) )
->set_meta( $this->get_css_id_control_meta() ),
];
}
protected function define_base_styles(): array {
$background_color_value = Background_Prop_Type::generate( [
'color' => Color_Prop_Type::generate( '#375EFB' ),
] );
$display_value = String_Prop_Type::generate( 'inline-block' );
$padding_value = Dimensions_Prop_Type::generate( [
'block-start' => Size_Prop_Type::generate( [
'size' => 12,
'unit' => 'px',
]),
'inline-end' => Size_Prop_Type::generate( [
'size' => 24,
'unit' => 'px',
]),
'block-end' => Size_Prop_Type::generate( [
'size' => 12,
'unit' => 'px',
]),
'inline-start' => Size_Prop_Type::generate( [
'size' => 24,
'unit' => 'px',
]),
]);
$border_radius_value = Size_Prop_Type::generate( [
'size' => 2,
'unit' => 'px',
] );
$border_width_value = Size_Prop_Type::generate( [
'size' => 0,
'unit' => 'px',
] );
$align_text_value = String_Prop_Type::generate( 'center' );
return [
'base' => Style_Definition::make()
->add_variant(
Style_Variant::make()
->add_prop( 'background', $background_color_value )
->add_prop( 'display', $display_value )
->add_prop( 'padding', $padding_value )
->add_prop( 'border-radius', $border_radius_value )
->add_prop( 'border-width', $border_width_value )
->add_prop( 'text-align', $align_text_value )
),
];
}
protected function get_templates(): array {
return [
'elementor/elements/atomic-button' => __DIR__ . '/atomic-button.html.twig',
];
}
}

View File

@@ -0,0 +1,4 @@
{% set classes = settings.classes | merge( [ base_styles.base ] ) | join(' ') %}
{% set id_attribute = settings._cssid is not empty ? 'id=' ~ settings._cssid | e('html_attr') : '' %}
<hr class="{{ classes }}" data-interaction-id="{{ id }}" data-interactions="{{ interactions | json_encode | e('html_attr') }}" {{ id_attribute }} {{ settings.attributes | raw }} />

View File

@@ -0,0 +1,108 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Atomic_Divider;
use Elementor\Modules\AtomicWidgets\Elements\Base\Atomic_Widget_Base;
use Elementor\Modules\AtomicWidgets\Elements\Base\Has_Template;
use Elementor\Modules\AtomicWidgets\PropTypes\Background_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Attributes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Classes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Color_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Size_Prop_Type;
use Elementor\Modules\AtomicWidgets\Styles\Style_Definition;
use Elementor\Modules\AtomicWidgets\Styles\Style_Variant;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Modules\AtomicWidgets\Controls\Types\Text_Control;
use Elementor\Modules\Components\PropTypes\Overridable_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Atomic_Divider extends Atomic_Widget_Base {
use Has_Template;
protected function get_css_id_control_meta(): array {
return [
'layout' => 'two-columns',
'topDivider' => false,
];
}
public static function get_element_type(): string {
return 'e-divider';
}
public function get_title() {
return esc_html__( 'Divider', 'elementor' );
}
public function get_keywords() {
return [ 'ato', 'atom', 'atoms', 'atomic', 'divider', 'hr', 'line', 'border', 'separator' ];
}
public function get_icon() {
return 'eicon-e-divider';
}
protected static function define_props_schema(): array {
return [
'classes' => Classes_Prop_Type::make()
->default( [] ),
'attributes' => Attributes_Prop_Type::make()->meta( Overridable_Prop_Type::ignore() ),
];
}
protected function define_atomic_controls(): array {
return [
Section::make()
->set_label( __( 'Settings', 'elementor' ) )
->set_id( 'settings' )
->set_items( $this->get_settings_controls() ),
];
}
protected function get_settings_controls(): array {
return [
Text_Control::bind_to( '_cssid' )
->set_label( __( 'ID', 'elementor' ) )
->set_meta( $this->get_css_id_control_meta() ),
];
}
protected function define_base_styles(): array {
$border_width_value = Size_Prop_Type::generate([
'size' => 0,
'unit' => 'px',
]);
$height_value = Size_Prop_Type::generate([
'size' => 1,
'unit' => 'px',
]);
$background_value = Background_Prop_Type::generate([
'color' => Color_Prop_Type::generate( '#000' ),
]);
return [
'base' => Style_Definition::make()
->add_variant(
Style_Variant::make()
->add_prop( 'border-width', $border_width_value )
->add_prop( 'border-color', 'transparent' )
->add_prop( 'border-style', 'none' )
->add_prop( 'background', $background_value )
->add_prop( 'height', $height_value )
),
];
}
protected function get_templates(): array {
return [
'elementor/elements/atomic-divider' => __DIR__ . '/atomic-divider.html.twig',
];
}
}

View File

@@ -0,0 +1,26 @@
{% if settings.title is not empty %}
{% set id_attribute = settings._cssid is not empty ? 'id=' ~ settings._cssid | e('html_attr') : '' %}
<{{ settings.tag | e('html_tag') }}
data-interaction-id="{{ id }}"
class="{{ settings.classes | merge( [ base_styles.base ] ) | join(' ') }}"
{{ id_attribute }}
{{ settings.attributes | raw }}
{% if interactions and interactions is not empty %}
data-interactions="{{ interactions | json_encode | e('html_attr') }}"
{% endif %}
>
{% set allowed_tags = '<b><strong><sup><sub><s><em><i><u><a><del><span><br>' %}
{% if settings.link.href %}
<{{ settings.link.tag | e('html_tag') }}
{% set linkAttr = settings.link.tag == 'a' ? 'href' : 'data-action-link' %}
{{ linkAttr }}="{{ settings.link.href | raw }}"
target="{{ settings.link.target }}"
class="{{ base_styles['link-base'] }}">
{{ settings.title | striptags(allowed_tags) | raw }}
</{{ settings.link.tag | e('html_tag') }}>
{% else %}
{{ settings.title | striptags(allowed_tags) | raw }}
{% endif %}
</{{ settings.tag | e('html_tag') }}>
{% endif %}

View File

@@ -0,0 +1,154 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Atomic_Heading;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Modules\AtomicWidgets\Controls\Types\Inline_Editing_Control;
use Elementor\Modules\AtomicWidgets\Controls\Types\Link_Control;
use Elementor\Modules\AtomicWidgets\Controls\Types\Select_Control;
use Elementor\Modules\AtomicWidgets\Controls\Types\Text_Control;
use Elementor\Modules\AtomicWidgets\Elements\Base\Atomic_Widget_Base;
use Elementor\Modules\AtomicWidgets\Elements\Base\Has_Template;
use Elementor\Modules\AtomicWidgets\PropTypes\Attributes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Classes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Html_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Link_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Size_Prop_Type;
use Elementor\Modules\AtomicWidgets\Styles\Style_Definition;
use Elementor\Modules\AtomicWidgets\Styles\Style_Variant;
use Elementor\Modules\Components\PropTypes\Overridable_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Atomic_Heading extends Atomic_Widget_Base {
use Has_Template;
const LINK_BASE_STYLE_KEY = 'link-base';
public static $widget_description = 'Display a heading with customizable tag, styles, and link options.';
public static function get_element_type(): string {
return 'e-heading';
}
public function get_title() {
return esc_html__( 'Heading', 'elementor' );
}
public function get_keywords() {
return [ 'ato', 'atom', 'atoms', 'atomic' ];
}
public function get_icon() {
return 'eicon-e-heading';
}
protected static function define_props_schema(): array {
return [
'classes' => Classes_Prop_Type::make()
->default( [] ),
'tag' => String_Prop_Type::make()
->enum( [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ] )
->default( 'h2' )
->description( 'The HTML tag for the heading element. Could be h1, h2, up to h6' ),
'title' => Html_Prop_Type::make()
->default( __( 'This is a title', 'elementor' ) )
->description( 'The text content of the heading.' ),
'link' => Link_Prop_Type::make(),
'attributes' => Attributes_Prop_Type::make()->meta( Overridable_Prop_Type::ignore() ),
];
}
protected function define_atomic_controls(): array {
$content_section = Section::make()
->set_label( __( 'Content', 'elementor' ) )
->set_items( [
Inline_Editing_Control::bind_to( 'title' )
->set_placeholder( __( 'Type your title here', 'elementor' ) )
->set_label( __( 'Title', 'elementor' ) ),
] );
return [
$content_section,
Section::make()
->set_label( __( 'Settings', 'elementor' ) )
->set_id( 'settings' )
->set_items( $this->get_settings_controls() ),
];
}
protected function get_settings_controls(): array {
return [
Select_Control::bind_to( 'tag' )
->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',
],
])
->set_label( __( 'Tag', 'elementor' ) ),
Link_Control::bind_to( 'link' )
->set_placeholder( __( 'Type or paste your URL', 'elementor' ) )
->set_label( __( 'Link', 'elementor' ) )
->set_meta( [
'topDivider' => true,
] ),
Text_Control::bind_to( '_cssid' )
->set_label( __( 'ID', 'elementor' ) )
->set_meta( $this->get_css_id_control_meta() ),
];
}
protected function define_base_styles(): array {
$margin_value = Size_Prop_Type::generate( [
'unit' => 'px',
'size' => 0 ,
] );
return [
'base' => Style_Definition::make()
->add_variant(
Style_Variant::make()
->add_prop( 'margin', $margin_value )
),
self::LINK_BASE_STYLE_KEY => Style_Definition::make()
->add_variant(
Style_Variant::make()
->add_prop( 'all', 'unset' )
->add_prop( 'cursor', 'pointer' )
),
];
}
protected function get_templates(): array {
return [
'elementor/elements/atomic-heading' => __DIR__ . '/atomic-heading.html.twig',
];
}
}

View File

@@ -0,0 +1,30 @@
{% if settings.image.src is not empty %}
{% set id_attribute = settings._cssid is not empty ? 'id=' ~ settings._cssid | e('html_attr') : '' %}
{% if settings.link.href %}
<{{ settings.link.tag | e('html_tag') }}
{% set linkAttr = settings.link.tag == 'a' ? 'href' : 'data-action-link' %}
{{ linkAttr }}="{{ settings.link.href | raw }}"
class="{{ base_styles['link-base'] }}"
target="{{ settings.link.target }}"
data-interaction-id="{{ id }}"
data-interactions="{{ interactions | json_encode | e('html_attr') }}"
>
{% endif %}
<img class="{{ base_styles['base'] }} {{ settings.classes | join(' ') }}"
{% if not settings.link.href %}
data-interaction-id="{{ id }}"
data-interactions="{{ interactions | json_encode | e('html_attr') }}"
{% endif %}
{{ id_attribute }} {{ settings.attributes | raw }}
{% for attr, value in settings.image %}
{% if attr == 'src' %}
src="{{ value | e('full_url') }}"
{% else %}
{{ attr | e('html_attr') }}="{{ value }}"
{% endif %}
{% endfor %}
/>
{% if settings.link.href %}
</{{ settings.link.tag | e('html_tag') }}>
{% endif %}
{% endif %}

View File

@@ -0,0 +1,111 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Atomic_Image;
use Elementor\Modules\AtomicWidgets\Controls\Types\Link_Control;
use Elementor\Modules\AtomicWidgets\Elements\Base\Has_Template;
use Elementor\Modules\AtomicWidgets\PropTypes\Classes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Image_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Link_Prop_Type;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Modules\AtomicWidgets\Elements\Base\Atomic_Widget_Base;
use Elementor\Modules\AtomicWidgets\PropTypes\Attributes_Prop_Type;
use Elementor\Modules\AtomicWidgets\Controls\Types\Image_Control;
use Elementor\Modules\AtomicWidgets\Utils\Image\Placeholder_Image;
use Elementor\Modules\AtomicWidgets\Styles\Style_Definition;
use Elementor\Modules\AtomicWidgets\Styles\Style_Variant;
use Elementor\Modules\AtomicWidgets\Controls\Types\Text_Control;
use Elementor\Modules\Components\PropTypes\Overridable_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Atomic_Image extends Atomic_Widget_Base {
use Has_Template;
public static $widget_description = 'Display an image with customizable styles and link options.';
const LINK_BASE_STYLE_KEY = 'link-base';
const BASE_STYLE_KEY = 'base';
public static function get_element_type(): string {
return 'e-image';
}
public function get_title() {
return esc_html__( 'Image', 'elementor' );
}
public function get_keywords() {
return [ 'ato', 'atom', 'atoms', 'atomic' ];
}
public function get_icon() {
return 'eicon-e-image';
}
protected static function define_props_schema(): array {
$props = [
'classes' => Classes_Prop_Type::make()
->default( [] ),
'image' => Image_Prop_Type::make()
->default_url( Placeholder_Image::get_placeholder_image() )
->default_size( 'full' ),
'link' => Link_Prop_Type::make(),
'attributes' => Attributes_Prop_Type::make()->meta( Overridable_Prop_Type::ignore() ),
];
return $props;
}
protected function define_atomic_controls(): array {
return [
Section::make()
->set_label( esc_html__( 'Content', 'elementor' ) )
->set_items( [
Image_Control::bind_to( 'image' )
->set_label( __( 'Image', 'elementor' ) ),
] ),
Section::make()
->set_label( __( 'Settings', 'elementor' ) )
->set_id( 'settings' )
->set_items( $this->get_settings_controls() ),
];
}
protected function get_settings_controls(): array {
return [
Link_Control::bind_to( 'link' )
->set_placeholder( __( 'Type or paste your URL', 'elementor' ) )
->set_label( __( 'Link', 'elementor' ) ),
Text_Control::bind_to( '_cssid' )
->set_label( __( 'ID', 'elementor' ) )
->set_meta( $this->get_css_id_control_meta() ),
];
}
protected function define_base_styles(): array {
return [
self::LINK_BASE_STYLE_KEY => Style_Definition::make()
->add_variant(
Style_Variant::make()
->add_prop( 'display', 'inherit' )
->add_prop( 'width', 'fit-content' )
),
self::BASE_STYLE_KEY => Style_Definition::make()
->add_variant(
Style_Variant::make()
->add_prop( 'display', 'block' )
),
];
}
protected function get_templates(): array {
return [
'elementor/elements/atomic-image' => __DIR__ . '/atomic-image.html.twig',
];
}
}

View File

@@ -0,0 +1,17 @@
{% if settings.paragraph is not empty %}
{% set id_attribute = settings._cssid is not empty ? 'id=' ~ settings._cssid | e('html_attr') : '' %}
<{{ settings.tag | e('html_tag') }} class="{{ settings.classes | merge( [ base_styles.base ] ) | join(' ') }}" data-interaction-id="{{ id }}" data-interactions="{{ interactions | json_encode | e('html_attr') }}" {{ id_attribute }} {{ settings.attributes | raw }}>
{% if settings.link.href %}
<{{ settings.link.tag | e('html_tag') }}
{% set linkAttr = settings.link.tag == 'a' ? 'href' : 'data-action-link' %}
{{ linkAttr }}="{{ settings.link.href | raw }}"
target="{{ settings.link.target }}"
class="{{ base_styles['link-base'] }}"
>
{{ settings.paragraph | striptags(allowed_tags) | raw }}
</{{ settings.link.tag | e('html_tag') }}>
{% else %}
{{ settings.paragraph | striptags('<b><strong><sup><sub><s><em><u><ul><ol><li><blockquote><a><del><span><br>') | raw }}
{% endif %}
</{{ settings.tag | e('html_tag') }}>
{% endif %}

View File

@@ -0,0 +1,136 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Atomic_Paragraph;
use Elementor\Modules\AtomicWidgets\Elements\Base\Atomic_Widget_Base;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Modules\AtomicWidgets\Controls\Types\Link_Control;
use Elementor\Modules\AtomicWidgets\Controls\Types\Select_Control;
use Elementor\Modules\AtomicWidgets\Elements\Base\Has_Template;
use Elementor\Modules\AtomicWidgets\PropTypes\Classes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Attributes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Html_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Link_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Size_Prop_Type;
use Elementor\Modules\AtomicWidgets\Styles\Style_Definition;
use Elementor\Modules\AtomicWidgets\Styles\Style_Variant;
use Elementor\Modules\AtomicWidgets\Controls\Types\Text_Control;
use Elementor\Modules\AtomicWidgets\Controls\Types\Inline_Editing_Control;
use Elementor\Modules\Components\PropTypes\Overridable_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Atomic_Paragraph extends Atomic_Widget_Base {
use Has_Template;
const LINK_BASE_STYLE_KEY = 'link-base';
public static $widget_description = 'Display a paragraph with customizable tag, styles, and link options.';
public static function get_element_type(): string {
return 'e-paragraph';
}
public function get_title() {
return esc_html__( 'Paragraph', 'elementor' );
}
public function get_keywords() {
return [ 'ato', 'atom', 'atoms', 'atomic' ];
}
public function get_icon() {
return 'eicon-paragraph';
}
protected static function define_props_schema(): array {
return [
'classes' => Classes_Prop_Type::make()
->default( [] ),
'paragraph' => Html_Prop_Type::make()
->default( __( 'Type your paragraph here', 'elementor' ) )
->description( 'The text content of the paragraph.' ),
'tag' => String_Prop_Type::make()
->enum( [ 'p', 'span' ] )
->default( 'p' ),
'link' => Link_Prop_Type::make(),
'attributes' => Attributes_Prop_Type::make()->meta( Overridable_Prop_Type::ignore() ),
];
}
protected function define_atomic_controls(): array {
return [
Section::make()
->set_label( __( 'Content', 'elementor' ) )
->set_items( [
Inline_Editing_Control::bind_to( 'paragraph' )
->set_placeholder( __( 'Type your paragraph here', 'elementor' ) )
->set_label( __( 'Paragraph', 'elementor' ) ),
] ),
Section::make()
->set_label( __( 'Settings', 'elementor' ) )
->set_id( 'settings' )
->set_items( $this->get_settings_controls() ),
];
}
protected function get_settings_controls(): array {
return [
Select_Control::bind_to( 'tag' )
->set_options([
[
'value' => 'p',
'label' => 'p',
],
[
'value' => 'span',
'label' => 'span',
],
])
->set_label( __( 'Tag', 'elementor' ) ),
Link_Control::bind_to( 'link' )
->set_placeholder( __( 'Type or paste your URL', 'elementor' ) )
->set_label( __( 'Link', 'elementor' ) )
->set_meta( [
'topDivider' => true,
] ),
Text_Control::bind_to( '_cssid' )
->set_label( __( 'ID', 'elementor' ) )
->set_meta( $this->get_css_id_control_meta() ),
];
}
protected function define_base_styles(): array {
$margin_value = Size_Prop_Type::generate( [
'unit' => 'px',
'size' => 0 ,
] );
return [
'base' => Style_Definition::make()
->add_variant(
Style_Variant::make()
->add_prop( 'margin', $margin_value )
),
self::LINK_BASE_STYLE_KEY => Style_Definition::make()
->add_variant(
Style_Variant::make()
->add_prop( 'all', 'unset' )
->add_prop( 'cursor', 'pointer' )
),
];
}
protected function get_templates(): array {
return [
'elementor/elements/atomic-paragraph' => __DIR__ . '/atomic-paragraph.html.twig',
];
}
}

View File

@@ -0,0 +1,240 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Atomic_Svg;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Modules\AtomicWidgets\Controls\Types\Link_Control;
use Elementor\Modules\AtomicWidgets\PropTypes\Classes_Prop_Type;
use Elementor\Modules\AtomicWidgets\Elements\Base\Atomic_Widget_Base;
use Elementor\Core\Utils\Svg\Svg_Sanitizer;
use Elementor\Modules\AtomicWidgets\Controls\Types\Svg_Control;
use Elementor\Modules\AtomicWidgets\PropTypes\Image_Src_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Attributes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Link_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Size_Prop_Type;
use Elementor\Modules\AtomicWidgets\Styles\Style_Definition;
use Elementor\Modules\AtomicWidgets\Styles\Style_Variant;
use Elementor\Modules\AtomicWidgets\Controls\Types\Text_Control;
use Elementor\Modules\Components\PropTypes\Overridable_Prop_Type;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Atomic_Svg extends Atomic_Widget_Base {
const BASE_STYLE_KEY = 'base';
const DEFAULT_SVG = 'images/default-svg.svg';
const DEFAULT_SVG_PATH = ELEMENTOR_ASSETS_PATH . self::DEFAULT_SVG;
const DEFAULT_SVG_URL = ELEMENTOR_ASSETS_URL . self::DEFAULT_SVG;
public static $widget_description = 'Display an SVG image with customizable styles and link options.';
public static function get_element_type(): string {
return 'e-svg';
}
public function get_title() {
return esc_html__( 'SVG', 'elementor' );
}
public function get_keywords() {
return [ 'ato', 'atom', 'atoms', 'atomic' ];
}
public function get_icon() {
return 'eicon-svg';
}
protected static function define_props_schema(): array {
return [
'classes' => Classes_Prop_Type::make()->default( [] ),
'svg' => Image_Src_Prop_Type::make()
->default_url( static::DEFAULT_SVG_URL )
->meta( 'is_svg', true ),
'link' => Link_Prop_Type::make(),
'attributes' => Attributes_Prop_Type::make()->meta( Overridable_Prop_Type::ignore() ),
];
}
protected function define_atomic_controls(): array {
return [
Section::make()
->set_label( esc_html__( 'Content', 'elementor' ) )
->set_items( [
Svg_Control::bind_to( 'svg' )
->set_label( __( 'SVG', 'elementor' ) ),
] ),
Section::make()
->set_label( __( 'Settings', 'elementor' ) )
->set_id( 'settings' )
->set_items( $this->get_settings_controls() ),
];
}
protected function get_settings_controls(): array {
return [
Link_Control::bind_to( 'link' )
->set_placeholder( __( 'Type or paste your URL', 'elementor' ) )
->set_label( __( 'Link', 'elementor' ) ),
Text_Control::bind_to( '_cssid' )
->set_label( __( 'ID', 'elementor' ) )
->set_meta( $this->get_css_id_control_meta() ),
];
}
protected function define_base_styles(): array {
$display_value = String_Prop_Type::generate( 'inline-block' );
$size = Size_Prop_Type::generate( [
'size' => 65,
'unit' => 'px',
] );
return [
self::BASE_STYLE_KEY => Style_Definition::make()
->add_variant(
Style_Variant::make()
->add_prop( 'display', $display_value )
->add_prop( 'width', $size )
->add_prop( 'height', $size )
),
];
}
protected function render() {
$settings = $this->get_atomic_settings();
$svg = $this->get_svg_content( $settings );
if ( ! $svg ) {
return;
}
$svg = new \WP_HTML_Tag_Processor( $svg );
if ( ! $svg->next_tag( 'svg' ) ) {
return;
}
$svg->set_attribute( 'fill', 'currentColor' );
$svg->set_attribute( 'data-interaction-id', $this->get_id() );
$interaction_ids = $this->get_interactions_ids();
if ( ! empty( $interaction_ids ) ) {
$svg->set_attribute( 'data-interactions', wp_json_encode( $interaction_ids ) );
}
$this->add_svg_style( $svg, 'width: 100%; height: 100%; overflow: unset;' );
$svg_html = ( new Svg_Sanitizer() )->sanitize( $svg->get_updated_html() );
$classes = array_filter( array_merge(
[ self::BASE_STYLE_KEY => $this->get_base_styles_dictionary()[ self::BASE_STYLE_KEY ] ],
$settings['classes']
) );
$classes_string = implode( ' ', $classes );
$cssid_attribute = ! empty( $settings['_cssid'] ) ? 'id="' . esc_attr( $settings['_cssid'] ) . '"' : '';
$all_attributes = trim( $cssid_attribute . ' ' . $settings['attributes'] );
$data_attributes_string = '';
if ( ! empty( $interaction_ids ) ) {
$data_attributes_string = sprintf(
'data-interaction-id="%s" data-interactions="%s"',
esc_attr( $this->get_id() ),
esc_attr( wp_json_encode( $interaction_ids ) )
);
}
$attributes_string = trim( $data_attributes_string . ' ' . $all_attributes );
if ( isset( $settings['link'] ) && ! empty( $settings['link']['href'] ) ) {
$html_tag = Utils::validate_html_tag( $settings['link']['tag'] ?? 'a' );
$link_attributes = $this->get_link_attributes( $settings['link'], true );
$link_attribute_key = $link_attributes['key'];
$link_destination = $link_attributes[ $link_attribute_key ];
$svg_html = sprintf(
'<%s %s="%s" target="%s" class="%s" %s>%s</%s>',
$html_tag,
$link_attribute_key,
$link_destination,
esc_attr( $settings['link']['target'] ),
esc_attr( $classes_string ),
$attributes_string,
$svg_html,
$html_tag
);
} else {
$svg_html = sprintf( '<div class="%s" %s>%s</div>', esc_attr( $classes_string ), $attributes_string, $svg_html );
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $svg_html;
}
private function get_svg_content( $settings ) {
$svg_data = $settings['svg'] ?? null;
if ( ! $svg_data ) {
return $this->get_default_svg_content();
}
if ( is_string( $svg_data ) ) {
return $this->fetch_svg_from_url( $svg_data );
}
if ( isset( $svg_data['id'] ) ) {
$content = Utils::file_get_contents(
get_attached_file( $svg_data['id'] )
);
if ( $content ) {
return $content;
}
}
if ( isset( $svg_data['url'] ) && static::DEFAULT_SVG_URL !== $svg_data['url'] ) {
return $this->fetch_svg_from_url( $svg_data['url'] );
}
return $this->get_default_svg_content();
}
private function fetch_svg_from_url( string $url ) {
if ( empty( $url ) || static::DEFAULT_SVG_URL === $url ) {
return $this->get_default_svg_content();
}
$content = wp_safe_remote_get( $url );
if ( ! is_wp_error( $content ) ) {
return $content['body'];
}
return $this->get_default_svg_content();
}
private function get_default_svg_content() {
$content = Utils::file_get_contents( static::DEFAULT_SVG_PATH );
return $content ? $content : null;
}
private function add_svg_style( &$svg, $new_style ) {
$svg_style = $svg->get_attribute( 'style' );
$svg_style = trim( (string) $svg_style );
if ( empty( $svg_style ) ) {
$svg_style = $new_style;
} else {
$svg_style = rtrim( $svg_style, ';' ) . '; ' . $new_style;
}
$svg->set_attribute( 'style', $svg_style );
}
}

View File

@@ -0,0 +1,142 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Atomic_Tabs;
use Elementor\Modules\AtomicWidgets\Elements\Base\Atomic_Element_Base;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Size_Prop_Type;
use Elementor\Modules\AtomicWidgets\Styles\Style_Definition;
use Elementor\Modules\AtomicWidgets\Styles\Style_Variant;
use Elementor\Modules\AtomicWidgets\Styles\Style_States;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Modules\AtomicWidgets\PropTypes\Classes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Attributes_Prop_Type;
use Elementor\Modules\AtomicWidgets\Elements\Base\Render_Context;
use Elementor\Modules\Components\PropTypes\Overridable_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Atomic_Tab_Content extends Atomic_Element_Base {
const BASE_STYLE_KEY = 'base';
public function __construct( $data = [], $args = null ) {
parent::__construct( $data, $args );
$this->meta( 'llm_support', false );
}
public static function get_type() {
return 'e-tab-content';
}
public static function get_element_type(): string {
return 'e-tab-content';
}
public function get_title() {
return esc_html__( 'Tab content', 'elementor' );
}
public function get_keywords() {
return [ 'ato', 'atom', 'atoms', 'atomic', 'tab', 'content', 'tabs' ];
}
public function get_icon() {
return 'eicon-layout';
}
public function should_show_in_panel() {
return false;
}
protected static function define_props_schema(): array {
return [
'classes' => Classes_Prop_Type::make()
->default( [] ),
'tab-id' => String_Prop_Type::make(),
'attributes' => Attributes_Prop_Type::make()->meta( Overridable_Prop_Type::ignore() ),
];
}
protected function define_atomic_controls(): array {
return [
Section::make()
->set_label( __( 'Settings', 'elementor' ) )
->set_id( 'settings' )
->set_items( [] ),
];
}
protected function define_atomic_style_states(): array {
$selected_state = Style_States::get_class_states_map()['selected'];
return [ $selected_state ];
}
protected function define_base_styles(): array {
$styles = [
'display' => String_Prop_Type::generate( 'block' ),
'padding' => Size_Prop_Type::generate( [
'size' => 10,
'unit' => 'px',
] ),
'min-width' => Size_Prop_Type::generate( [
'size' => 30,
'unit' => 'px',
] ),
];
return [
static::BASE_STYLE_KEY => Style_Definition::make()
->add_variant(
Style_Variant::make()
->add_props( $styles )
),
];
}
protected function define_initial_attributes() {
return [
'role' => 'tabpanel',
];
}
protected function add_render_attributes() {
parent::add_render_attributes();
$settings = $this->get_atomic_settings();
$base_style_class = $this->get_base_styles_dictionary()[ static::BASE_STYLE_KEY ];
$initial_attributes = $this->define_initial_attributes();
$tabs_context = Render_Context::get( Atomic_Tabs::class );
$default_active_tab = $tabs_context['default-active-tab'];
$get_tab_content_index = $tabs_context['get-tab-content-index'];
$tabs_id = $tabs_context['tabs-id'];
$index = $get_tab_content_index( $this->get_id() );
$is_active = $default_active_tab === $index;
$attributes = [
'class' => [
'e-con',
'e-atomic-element',
$base_style_class,
...( $settings['classes'] ?? [] ),
],
'x-bind' => 'tabContent',
'id' => Atomic_Tabs::get_tab_content_id( $tabs_id, $index ),
'aria-labelledby' => Atomic_Tabs::get_tab_id( $tabs_id, $index ),
];
if ( ! $is_active ) {
$attributes['hidden'] = 'true';
$attributes['style'] = 'display: none;';
}
if ( ! empty( $settings['_cssid'] ) ) {
$attributes['id'] = esc_attr( $settings['_cssid'] );
}
$this->add_render_attribute( '_wrapper', array_merge( $initial_attributes, $attributes ) );
}
}

View File

@@ -0,0 +1,198 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Atomic_Tabs;
use Elementor\Modules\AtomicWidgets\Elements\Base\Atomic_Element_Base;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Size_Prop_Type;
use Elementor\Modules\AtomicWidgets\Styles\Style_Definition;
use Elementor\Modules\AtomicWidgets\Styles\Style_Variant;
use Elementor\Modules\AtomicWidgets\Styles\Style_States;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Modules\AtomicWidgets\PropTypes\Classes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Html_Prop_Type;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Paragraph\Atomic_Paragraph;
use Elementor\Modules\AtomicWidgets\PropTypes\Attributes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Color_Prop_Type;
use Elementor\Modules\AtomicWidgets\Elements\Base\Render_Context;
use Elementor\Modules\AtomicWidgets\PropTypes\Background_Prop_Type;
use Elementor\Modules\Components\PropTypes\Overridable_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Atomic_Tab extends Atomic_Element_Base {
const BASE_STYLE_KEY = 'base';
public function __construct( $data = [], $args = null ) {
parent::__construct( $data, $args );
$this->meta( 'llm_support', false );
}
public static function get_type() {
return 'e-tab';
}
public static function get_element_type(): string {
return 'e-tab';
}
public function get_title() {
return esc_html__( 'Tab trigger', 'elementor' );
}
public function get_keywords() {
return [ 'ato', 'atom', 'atoms', 'atomic' ];
}
public function get_icon() {
return 'eicon-layout';
}
public function should_show_in_panel() {
return false;
}
protected static function define_props_schema(): array {
return [
'classes' => Classes_Prop_Type::make()
->default( [] ),
'attributes' => Attributes_Prop_Type::make()->meta( Overridable_Prop_Type::ignore() ),
];
}
protected function define_atomic_controls(): array {
return [
Section::make()
->set_label( __( 'Settings', 'elementor' ) )
->set_id( 'settings' )
->set_items( [] ),
];
}
protected function define_atomic_style_states(): array {
$selected_state = Style_States::get_class_states_map()['selected'];
return [ $selected_state ];
}
protected function define_base_styles(): array {
$styles = [
'display' => String_Prop_Type::generate( 'block' ),
'cursor' => String_Prop_Type::generate( 'pointer' ),
'color' => Color_Prop_Type::generate( '#0C0D0E' ),
'border-style' => String_Prop_Type::generate( 'solid' ),
'border-color' => Color_Prop_Type::generate( '#E0E0E0' ),
'border-width' => Size_Prop_Type::generate( [
'size' => 2,
'unit' => 'px',
]),
'padding' => Size_Prop_Type::generate( [
'size' => 8,
'unit' => 'px',
]),
'width' => Size_Prop_Type::generate( [
'size' => 160,
'unit' => 'px',
]),
'background' => Background_Prop_Type::generate( [
'color' => Color_Prop_Type::generate( '#FFFFFF' ),
]),
];
$selected_styles = [
'outline-width' => Size_Prop_Type::generate( [
'size' => 0,
'unit' => 'px',
]),
'border-color' => Color_Prop_Type::generate( '#0C0D0E' ),
];
$hover_styles = [
'background' => Background_Prop_Type::generate( [
'color' => Color_Prop_Type::generate( '#E0E0E0' ),
]),
];
return [
static::BASE_STYLE_KEY => Style_Definition::make()
->add_variant(
Style_Variant::make()
->add_props( $styles )
)
->add_variant(
Style_Variant::make()
->set_state( Style_States::SELECTED )
->add_props( $selected_styles )
)
->add_variant(
Style_Variant::make()
->set_state( Style_States::FOCUS )
->add_props( $selected_styles )
)
->add_variant(
Style_Variant::make()
->set_state( Style_States::HOVER )
->add_props( $hover_styles )
),
];
}
protected function define_initial_attributes() {
return [
'role' => 'tab',
'tabindex' => '-1',
];
}
protected function define_default_html_tag() {
return 'button';
}
protected function define_default_children() {
return [
Atomic_Paragraph::generate()
->settings( [
'paragraph' => Html_Prop_Type::generate( 'Tab' ),
'tag' => String_Prop_Type::generate( 'span' ),
] )
->build(),
];
}
protected function add_render_attributes() {
parent::add_render_attributes();
$settings = $this->get_atomic_settings();
$base_style_class = $this->get_base_styles_dictionary()[ static::BASE_STYLE_KEY ];
$initial_attributes = $this->define_initial_attributes();
$tabs_context = Render_Context::get( Atomic_Tabs::class );
$default_active_tab = $tabs_context['default-active-tab'];
$get_tab_index = $tabs_context['get-tab-index'];
$tabs_id = $tabs_context['tabs-id'];
$index = $get_tab_index( $this->get_id() );
$is_active = $default_active_tab === $index;
$attributes = [
'class' => [
'e-con',
'e-atomic-element',
$base_style_class,
...( $settings['classes'] ?? [] ),
],
'tabindex' => $is_active ? '0' : '-1',
'aria-selected' => $is_active ? 'true' : 'false',
'x-bind' => 'tab',
'x-ref' => $this->get_id(),
'id' => Atomic_Tabs::get_tab_id( $tabs_id, $index ),
'aria-controls' => Atomic_Tabs::get_tab_content_id( $tabs_id, $index ),
];
if ( ! empty( $settings['_cssid'] ) ) {
$attributes['id'] = esc_attr( $settings['_cssid'] );
}
$this->add_render_attribute( '_wrapper', array_merge( $initial_attributes, $attributes ) );
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Atomic_Tabs;
use Elementor\Modules\AtomicWidgets\Elements\Base\Atomic_Element_Base;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\Boolean_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Size_Prop_Type;
use Elementor\Modules\AtomicWidgets\Styles\Style_Definition;
use Elementor\Modules\AtomicWidgets\Styles\Style_Variant;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Modules\AtomicWidgets\Controls\Types\Text_Control;
use Elementor\Modules\AtomicWidgets\PropTypes\Classes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Attributes_Prop_Type;
use Elementor\Modules\Components\PropTypes\Overridable_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Atomic_Tabs_Content_Area extends Atomic_Element_Base {
const BASE_STYLE_KEY = 'base';
public function __construct( $data = [], $args = null ) {
parent::__construct( $data, $args );
$this->meta( 'llm_support', false );
}
public static function get_type() {
return 'e-tabs-content-area';
}
public static function get_element_type(): string {
return 'e-tabs-content-area';
}
public function get_title() {
return esc_html__( 'Tabs content area', 'elementor' );
}
public function get_keywords() {
return [ 'ato', 'atom', 'atoms', 'atomic' ];
}
public function get_icon() {
return 'eicon-tab-content';
}
public function should_show_in_panel() {
return false;
}
protected static function define_props_schema(): array {
return [
'classes' => Classes_Prop_Type::make()
->default( [] ),
'attributes' => Attributes_Prop_Type::make()->meta( Overridable_Prop_Type::ignore() ),
];
}
protected function define_atomic_controls(): array {
return [
Section::make()
->set_label( __( 'Settings', 'elementor' ) )
->set_id( 'settings' )
->set_items( [
Text_Control::bind_to( '_cssid' )
->set_label( __( 'ID', 'elementor' ) )
->set_meta( [
'layout' => 'two-columns',
] ),
] ),
];
}
protected function define_base_styles(): array {
$styles = [
'display' => String_Prop_Type::generate( 'block' ),
];
return [
static::BASE_STYLE_KEY => Style_Definition::make()
->add_variant(
Style_Variant::make()
->add_props( $styles )
),
];
}
protected function add_render_attributes() {
parent::add_render_attributes();
$settings = $this->get_atomic_settings();
$base_style_class = $this->get_base_styles_dictionary()[ static::BASE_STYLE_KEY ];
$initial_attributes = $this->define_initial_attributes();
$attributes = [
'class' => [
'e-con',
'e-atomic-element',
$base_style_class,
...( $settings['classes'] ?? [] ),
],
];
if ( ! empty( $settings['_cssid'] ) ) {
$attributes['id'] = esc_attr( $settings['_cssid'] );
}
$this->add_render_attribute( '_wrapper', array_merge( $initial_attributes, $attributes ) );
}
}

View File

@@ -0,0 +1,115 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Atomic_Tabs;
use Elementor\Modules\AtomicWidgets\Elements\Base\Atomic_Element_Base;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
use Elementor\Modules\AtomicWidgets\Styles\Style_Definition;
use Elementor\Modules\AtomicWidgets\Styles\Style_Variant;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Modules\AtomicWidgets\Controls\Types\Text_Control;
use Elementor\Modules\AtomicWidgets\PropTypes\Classes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Attributes_Prop_Type;
use Elementor\Modules\Components\PropTypes\Overridable_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Atomic_Tabs_Menu extends Atomic_Element_Base {
const BASE_STYLE_KEY = 'base';
public function __construct( $data = [], $args = null ) {
parent::__construct( $data, $args );
$this->meta( 'llm_support', false );
}
public static function get_type() {
return 'e-tabs-menu';
}
public static function get_element_type(): string {
return 'e-tabs-menu';
}
public function get_title() {
return esc_html__( 'Tabs menu', 'elementor' );
}
public function get_keywords() {
return [ 'ato', 'atom', 'atoms', 'atomic' ];
}
public function get_icon() {
return 'eicon-tab-menu';
}
public function should_show_in_panel() {
return false;
}
public function define_initial_attributes(): array {
return [
'role' => 'tablist',
];
}
protected static function define_props_schema(): array {
return [
'classes' => Classes_Prop_Type::make()
->default( [] ),
'attributes' => Attributes_Prop_Type::make()->meta( Overridable_Prop_Type::ignore() ),
];
}
protected function define_atomic_controls(): array {
return [
Section::make()
->set_label( __( 'Settings', 'elementor' ) )
->set_id( 'settings' )
->set_items( [
Text_Control::bind_to( '_cssid' )
->set_label( __( 'ID', 'elementor' ) )
->set_meta( [
'layout' => 'two-columns',
] ),
] ),
];
}
protected function define_base_styles(): array {
$styles = [
'display' => String_Prop_Type::generate( 'flex' ),
'justify-content' => String_Prop_Type::generate( 'center' ),
];
return [
static::BASE_STYLE_KEY => Style_Definition::make()
->add_variant(
Style_Variant::make()
->add_props( $styles )
),
];
}
protected function add_render_attributes() {
parent::add_render_attributes();
$settings = $this->get_atomic_settings();
$base_style_class = $this->get_base_styles_dictionary()[ static::BASE_STYLE_KEY ];
$initial_attributes = $this->define_initial_attributes();
$attributes = [
'class' => [
'e-con',
'e-atomic-element',
$base_style_class,
...( $settings['classes'] ?? [] ),
],
];
if ( ! empty( $settings['_cssid'] ) ) {
$attributes['id'] = esc_attr( $settings['_cssid'] );
}
$this->add_render_attribute( '_wrapper', array_merge( $initial_attributes, $attributes ) );
}
}

View File

@@ -0,0 +1,268 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Atomic_Tabs;
use Elementor\Modules\AtomicWidgets\Elements\Base\Atomic_Element_Base;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\Number_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Size_Prop_Type;
use Elementor\Modules\AtomicWidgets\Styles\Style_Definition;
use Elementor\Modules\AtomicWidgets\Styles\Style_Variant;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Modules\AtomicWidgets\Controls\Types\Text_Control;
use Elementor\Modules\AtomicWidgets\PropTypes\Classes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Attributes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Dimensions_Prop_Type;
use Elementor\Modules\AtomicWidgets\Controls\Types\Elements\Tabs_Control;
use Elementor\Modules\AtomicWidgets\Elements\Loader\Frontend_Assets_Loader;
use Elementor\Modules\Components\PropTypes\Overridable_Prop_Type;
use Elementor\Core\Utils\Collection;
use Elementor\Utils;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Atomic_Tabs extends Atomic_Element_Base {
const BASE_STYLE_KEY = 'base';
const ELEMENT_TYPE_TABS_MENU = 'e-tabs-menu';
const ELEMENT_TYPE_TABS_CONTENT_AREA = 'e-tabs-content-area';
const ELEMENT_TYPE_TAB = 'e-tab';
const ELEMENT_TYPE_TAB_CONTENT = 'e-tab-content';
public static $widget_description = 'Create a tabbed interface with customizable tabs and content areas. LLM support: Each child element will be represented as a tab, the menu auto-generates based on the children';
public function __construct( $data = [], $args = null ) {
parent::__construct( $data, $args );
$this->meta( 'is_container', true );
}
public static function get_type() {
return 'e-tabs';
}
public static function get_element_type(): string {
return 'e-tabs';
}
public function get_title() {
return esc_html__( 'Tabs', 'elementor' );
}
public function get_keywords() {
return [ 'ato', 'atom', 'atoms', 'atomic' ];
}
public function get_icon() {
return 'eicon-tabs';
}
protected static function define_props_schema(): array {
return [
'classes' => Classes_Prop_Type::make()
->default( [] ),
'default-active-tab' => Number_Prop_Type::make()
->default( 0 )
->meta( Overridable_Prop_Type::ignore() ),
'attributes' => Attributes_Prop_Type::make()->meta( Overridable_Prop_Type::ignore() ),
];
}
protected function define_atomic_controls(): array {
return [
Section::make()
->set_label( __( 'Content', 'elementor' ) )
->set_id( 'content' )
->set_items( [
Tabs_Control::make()
->set_label( __( 'Menu items', 'elementor' ) )
->set_meta( [
'layout' => 'custom',
] ),
] ),
Section::make()
->set_label( __( 'Settings', 'elementor' ) )
->set_id( 'settings' )
->set_items( [
Text_Control::bind_to( '_cssid' )
->set_label( __( 'ID', 'elementor' ) )
->set_meta( [
'layout' => 'two-columns',
] ),
] ),
];
}
protected function define_base_styles(): array {
$styles = [
'display' => String_Prop_Type::generate( 'flex' ),
'flex-direction' => String_Prop_Type::generate( 'column' ),
'gap' => Size_Prop_Type::generate( [
'size' => 30,
'unit' => 'px',
]),
'padding' => Dimensions_Prop_Type::generate( [
'block-start' => Size_Prop_Type::generate( [
'size' => 0,
'unit' => 'px',
]),
] ),
];
return [
static::BASE_STYLE_KEY => Style_Definition::make()
->add_variant(
Style_Variant::make()
->add_props( $styles )
),
];
}
protected function define_default_children() {
$default_tab_count = 3;
$tab_elements = [];
$tab_content_elements = [];
foreach ( range( 1, $default_tab_count ) as $i ) {
$tab_elements[] = Atomic_Tab::generate()
->editor_settings( [
'title' => "Tab {$i} trigger",
'initial_position' => $i,
] )
->is_locked( true )
->build();
$tab_content_elements[] = Atomic_Tab_Content::generate()
->is_locked( true )
->editor_settings( [
'title' => "Tab {$i} content",
'initial_position' => $i,
] )
->build();
}
$tabs_menu = Atomic_Tabs_Menu::generate()
->children( $tab_elements )
->is_locked( true )
->build();
$tabs_content_area = Atomic_Tabs_Content_Area::generate()
->children( $tab_content_elements )
->is_locked( true )
->build();
return [
$tabs_menu,
$tabs_content_area,
];
}
public function get_script_depends() {
$global_depends = parent::get_script_depends();
if ( Plugin::$instance->preview->is_preview_mode() ) {
return array_merge( $global_depends, [ 'elementor-tabs-handler', 'elementor-tabs-preview-handler' ] );
}
return array_merge( $global_depends, [ 'elementor-tabs-handler' ] );
}
public function register_frontend_handlers() {
$assets_url = ELEMENTOR_ASSETS_URL;
$min_suffix = ( Utils::is_script_debug() || Utils::is_elementor_tests() ) ? '' : '.min';
wp_register_script(
'elementor-tabs-handler',
"{$assets_url}js/tabs-handler{$min_suffix}.js",
[ Frontend_Assets_Loader::FRONTEND_HANDLERS_HANDLE, Frontend_Assets_Loader::ALPINEJS_HANDLE ],
ELEMENTOR_VERSION,
true
);
wp_register_script(
'elementor-tabs-preview-handler',
"{$assets_url}js/tabs-preview-handler{$min_suffix}.js",
[ Frontend_Assets_Loader::FRONTEND_HANDLERS_HANDLE, Frontend_Assets_Loader::ALPINEJS_HANDLE ],
ELEMENTOR_VERSION,
true
);
}
private function get_filtered_children_ids( $parent_element, $child_type ) {
if ( ! $parent_element ) {
return [];
}
return Collection::make( $parent_element->get_children() )
->filter( fn( $element ) => $element->get_type() === $child_type )
->map( fn( $element ) => $element->get_id() )
->flip()
->all();
}
private function get_tab_index( $tab_id ) {
$direct_children = Collection::make( $this->get_children() );
$tabs_menu = $direct_children->filter( fn( $child ) => $child->get_type() === self::ELEMENT_TYPE_TABS_MENU )->first();
$tab_ids = $this->get_filtered_children_ids( $tabs_menu, self::ELEMENT_TYPE_TAB );
return $tab_ids[ $tab_id ];
}
private function get_tab_content_index( $tab_content_id ) {
$direct_children = Collection::make( $this->get_children() );
$tabs_content_area = $direct_children->filter( fn( $child ) => $child->get_type() === self::ELEMENT_TYPE_TABS_CONTENT_AREA )->first();
$tab_content_ids = $this->get_filtered_children_ids( $tabs_content_area, self::ELEMENT_TYPE_TAB_CONTENT );
return $tab_content_ids[ $tab_content_id ];
}
protected function define_render_context(): array {
$default_active_tab = $this->get_atomic_setting( 'default-active-tab' );
return [
'context' => [
'default-active-tab' => $default_active_tab,
'get-tab-index' => fn( $tab_id ) => $this->get_tab_index( $tab_id ),
'get-tab-content-index' => fn( $tab_content_id ) => $this->get_tab_content_index( $tab_content_id ),
'tabs-id' => $this->get_id(),
],
];
}
protected function add_render_attributes() {
parent::add_render_attributes();
$settings = $this->get_atomic_settings();
$base_style_class = $this->get_base_styles_dictionary()[ static::BASE_STYLE_KEY ];
$initial_attributes = $this->define_initial_attributes();
$default_active_tab = $settings['default-active-tab'] ?? 0;
$default_active_tab_id = static::get_tab_id( $this->get_id(), $default_active_tab );
$attributes = [
'class' => [
'e-con',
'e-atomic-element',
$base_style_class,
...( $settings['classes'] ?? [] ),
],
'x-data' => 'eTabs' . $this->get_id(),
'data-e-settings' => json_encode( [ 'default-active-tab' => esc_js( $default_active_tab_id ) ] ),
];
if ( ! empty( $settings['_cssid'] ) ) {
$attributes['id'] = esc_attr( $settings['_cssid'] );
}
$this->add_render_attribute( '_wrapper', array_merge( $initial_attributes, $attributes ) );
}
public static function get_tab_id( $tabs_id, $index ) {
return "{$tabs_id}-tab-{$index}";
}
public static function get_tab_content_id( $tabs_id, $index ) {
return "{$tabs_id}-tab-content-{$index}";
}
}

View File

@@ -0,0 +1,87 @@
import { register } from '@elementor/frontend-handlers';
import { Alpine } from '@elementor/alpinejs';
import { TAB_ELEMENT_TYPE, TAB_CONTENT_ELEMENT_TYPE, getTabId, getTabContentId, getIndex, getNextTab } from './utils';
const SELECTED_CLASS = 'e--selected';
register( {
elementType: 'e-tabs',
id: 'e-tabs-handler',
callback: ( { element, settings } ) => {
const tabsId = element.dataset.id;
Alpine.data( `eTabs${ tabsId }`, () => ( {
activeTab: settings[ 'default-active-tab' ],
navigateTabs( { key, target: tab } ) {
const nextTab = getNextTab( key, tab );
nextTab.focus();
},
tab: {
':id'() {
const index = getIndex( this.$el, TAB_ELEMENT_TYPE );
return getTabId( tabsId, index );
},
'@click'() {
const id = this.$el.id;
this.activeTab = id;
},
'@keydown.arrow-right.prevent'( event ) {
this.navigateTabs( event );
},
'@keydown.arrow-left.prevent'( event ) {
this.navigateTabs( event );
},
':class'() {
const id = this.$el.id;
return this.activeTab === id ? SELECTED_CLASS : '';
},
':aria-selected'() {
const id = this.$el.id;
return this.activeTab === id ? 'true' : 'false';
},
':tabindex'() {
const id = this.$el.id;
return this.activeTab === id ? '0' : '-1';
},
':aria-controls'() {
const index = getIndex( this.$el, TAB_ELEMENT_TYPE );
return getTabContentId( tabsId, index );
},
},
tabContent: {
':aria-labelledby'() {
const index = getIndex( this.$el, TAB_CONTENT_ELEMENT_TYPE );
return getTabId( tabsId, index );
},
'x-show'() {
const index = getIndex( this.$el, TAB_CONTENT_ELEMENT_TYPE );
const tabId = getTabId( tabsId, index );
const isActive = this.activeTab === tabId;
this.$nextTick( () => {
this.$el.classList.toggle( SELECTED_CLASS, isActive );
} );
return isActive;
},
':id'() {
const index = getIndex( this.$el, TAB_CONTENT_ELEMENT_TYPE );
return getTabContentId( tabsId, index );
},
},
} ) );
},
} );

View File

@@ -0,0 +1,30 @@
import { register } from '@elementor/frontend-handlers';
import { Alpine, refreshTree } from '@elementor/alpinejs';
import { TAB_ELEMENT_TYPE, TAB_CONTENT_ELEMENT_TYPE, getTabId, getIndex } from './utils';
register( {
elementType: 'e-tabs',
id: 'e-tabs-preview-handler',
callback: ( { element, signal, listenToChildren } ) => {
window?.parent.addEventListener( 'elementor/navigator/item/click', ( event ) => {
const { id, type } = event.detail;
if ( type !== TAB_ELEMENT_TYPE && type !== TAB_CONTENT_ELEMENT_TYPE ) {
return;
}
const targetElement = Alpine.$data( element ).$refs[ id ];
if ( ! targetElement ) {
return;
}
const targetIndex = getIndex( targetElement, type );
Alpine.$data( element ).activeTab = getTabId( element.dataset.id, targetIndex );
}, { signal } );
// Re-initialize Alpine to sync with editor DOM manipulations that bypass Alpine's reactivity.
listenToChildren( [ TAB_ELEMENT_TYPE, TAB_CONTENT_ELEMENT_TYPE ] )
.render( () => refreshTree( element ) );
},
} );

View File

@@ -0,0 +1,44 @@
export const TAB_ELEMENT_TYPE = 'e-tab';
export const TAB_CONTENT_ELEMENT_TYPE = 'e-tab-content';
export const TABS_CONTENT_AREA_ELEMENT_TYPE = 'e-tabs-content-area';
export const TABS_MENU_ELEMENT_TYPE = 'e-tabs-menu';
const NAVIGATE_UP_KEYS = [ 'ArrowUp', 'ArrowLeft' ];
const NAVIGATE_DOWN_KEYS = [ 'ArrowDown', 'ArrowRight' ];
export const getTabId = ( tabsId, tabIndex ) => {
return `${ tabsId }-tab-${ tabIndex }`;
};
export const getTabContentId = ( tabsId, tabIndex ) => {
return `${ tabsId }-tab-content-${ tabIndex }`;
};
export const getChildren = ( el, elementType ) => {
const parent = el.parentElement;
return Array.from( parent.children ).filter( ( child ) => {
return child.dataset.element_type === elementType;
} );
};
export const getIndex = ( el, elementType ) => {
const children = getChildren( el, elementType );
return children.indexOf( el );
};
export const getNextTab = ( key, tab ) => {
const tabs = getChildren( tab, TAB_ELEMENT_TYPE );
const tabsLength = tabs.length;
const currentIndex = getIndex( tab, TAB_ELEMENT_TYPE );
if ( NAVIGATE_DOWN_KEYS.includes( key ) ) {
return tabs[ ( currentIndex + 1 ) % tabsLength ];
}
if ( NAVIGATE_UP_KEYS.includes( key ) ) {
return tabs[ ( currentIndex - 1 + tabsLength ) % tabsLength ];
}
};

View File

@@ -0,0 +1,18 @@
{% if settings.source is not empty %}
{% set id_attribute = settings._cssid is not empty ? 'id=' ~ settings._cssid | e('html_attr') : '' %}
{% set classes = settings.classes | merge( [ base_styles.base ] ) | join(' ') %}
{% set data_settings = {
'source': settings.source,
'autoplay': settings.autoplay,
'mute': settings.mute,
'controls': settings.player_controls,
'cc_load_policy': settings.captions,
'loop': settings.loop,
'rel': settings.rel,
'start': settings.start,
'end': settings.end,
'privacy': settings.privacy_mode,
'lazyload': settings.lazyload,
} %}
<div data-id="{{ id }}" data-interaction-id="{{ id }}" data-e-type="{{ type }}" data-interactions="{{ interactions | json_encode | e('html_attr') }}" {{ id_attribute }} class="{{ classes }}" {{ settings.attributes | raw }} data-settings="{{ data_settings|json_encode|e('html_attr') }}"></div>
{% endif %}

View File

@@ -0,0 +1,144 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Atomic_Youtube;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Modules\AtomicWidgets\Controls\Types\Switch_Control;
use Elementor\Modules\AtomicWidgets\Controls\Types\Text_Control;
use Elementor\Modules\AtomicWidgets\DynamicTags\Dynamic_Prop_Type;
use Elementor\Modules\AtomicWidgets\Elements\Base\Atomic_Widget_Base;
use Elementor\Modules\AtomicWidgets\Elements\Base\Has_Template;
use Elementor\Modules\AtomicWidgets\PropTypes\Classes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Attributes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\Boolean_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
use Elementor\Modules\AtomicWidgets\Styles\Style_Definition;
use Elementor\Modules\AtomicWidgets\Styles\Style_Variant;
use Elementor\Modules\AtomicWidgets\Elements\Loader\Frontend_Assets_Loader;
use Elementor\Modules\Components\PropTypes\Overridable_Prop_Type;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Atomic_Youtube extends Atomic_Widget_Base {
use Has_Template;
protected function get_css_id_control_meta(): array {
return [
'layout' => 'two-columns',
'topDivider' => false,
];
}
public static function get_element_type(): string {
return 'e-youtube';
}
public function get_title() {
return esc_html__( 'YouTube', 'elementor' );
}
public function get_keywords() {
return [ 'ato', 'atom', 'atoms', 'atomic' ];
}
public function get_icon() {
return 'eicon-e-youtube';
}
protected static function define_props_schema(): array {
return [
'classes' => Classes_Prop_Type::make()
->default( [] ),
'source' => String_Prop_Type::make()
->default( 'https://www.youtube.com/watch?v=XHOmBV4js_E' ),
'start' => String_Prop_Type::make()->meta( Dynamic_Prop_Type::ignore() ),
'end' => String_Prop_Type::make()->meta( Dynamic_Prop_Type::ignore() ),
'autoplay' => Boolean_Prop_Type::make()->default( false ),
'mute' => Boolean_Prop_Type::make()->default( false ),
'loop' => Boolean_Prop_Type::make()->default( false ),
'lazyload' => Boolean_Prop_Type::make()->default( false ),
'player_controls' => Boolean_Prop_Type::make()->default( true ),
'captions' => Boolean_Prop_Type::make()->default( false ),
'privacy_mode' => Boolean_Prop_Type::make()->default( false ),
'rel' => Boolean_Prop_Type::make()->default( true ),
'attributes' => Attributes_Prop_Type::make()->meta( Overridable_Prop_Type::ignore() ),
];
}
protected function define_atomic_controls(): array {
return [
Section::make()
->set_label( __( 'Content', 'elementor' ) )
->set_items( [
Text_Control::bind_to( 'source' )
->set_placeholder( esc_html__( 'Type or paste your URL', 'elementor' ) )
->set_label( esc_html__( 'YouTube URL', 'elementor' ) ),
Text_Control::bind_to( 'start' )->set_label( esc_html__( 'Start time', 'elementor' ) ),
Text_Control::bind_to( 'end' )->set_label( esc_html__( 'End time', 'elementor' ) ),
Switch_Control::bind_to( 'autoplay' )->set_label( esc_html__( 'Autoplay', 'elementor' ) ),
Switch_Control::bind_to( 'mute' )->set_label( esc_html__( 'Mute', 'elementor' ) ),
Switch_Control::bind_to( 'loop' )->set_label( esc_html__( 'Loop', 'elementor' ) ),
Switch_Control::bind_to( 'lazyload' )->set_label( esc_html__( 'Lazy load', 'elementor' ) ),
Switch_Control::bind_to( 'player_controls' )->set_label( esc_html__( 'Player controls', 'elementor' ) ),
Switch_Control::bind_to( 'captions' )->set_label( esc_html__( 'Captions', 'elementor' ) ),
Switch_Control::bind_to( 'privacy_mode' )->set_label( esc_html__( 'Privacy mode', 'elementor' ) ),
Switch_Control::bind_to( 'rel' )->set_label( esc_html__( 'Related videos', 'elementor' ) ),
] ),
Section::make()
->set_label( __( 'Settings', 'elementor' ) )
->set_id( 'settings' )
->set_items( $this->get_settings_controls() ),
];
}
protected function get_settings_controls(): array {
return [
Text_Control::bind_to( '_cssid' )
->set_label( __( 'ID', 'elementor' ) )
->set_meta( $this->get_css_id_control_meta() ),
];
}
protected function define_base_styles(): array {
return [
'base' => Style_Definition::make()
->add_variant(
Style_Variant::make()
->add_prop( 'aspect-ratio', String_Prop_Type::generate( '16/9' ) )
->add_prop( 'overflow', String_Prop_Type::generate( 'hidden' ) )
),
];
}
public function get_script_depends() {
return array_merge(
parent::get_script_depends(),
[ 'elementor-youtube-handler' ],
);
}
public function register_frontend_handlers() {
$assets_url = ELEMENTOR_ASSETS_URL;
$min_suffix = ( Utils::is_script_debug() || Utils::is_elementor_tests() ) ? '' : '.min';
wp_register_script(
'elementor-youtube-handler',
"{$assets_url}js/youtube-handler{$min_suffix}.js",
[ Frontend_Assets_Loader::FRONTEND_HANDLERS_HANDLE ],
ELEMENTOR_VERSION,
true
);
}
protected function get_templates(): array {
return [
'elementor/elements/atomic-youtube' => __DIR__ . '/atomic-youtube.html.twig',
];
}
}

View File

@@ -0,0 +1,126 @@
import { register } from '@elementor/frontend-handlers';
const getYoutubeVideoIdFromUrl = ( url ) => {
const regex = /^(?:https?:\/\/)?(?:www\.)?(?:m\.)?(?:youtu\.be\/|youtube\.com\/(?:(?:watch)?\?(?:.*&)?vi?=|(?:embed|v|vi|user|shorts)\/))([^?&"'>]+)/;
const match = url.match( regex );
return match ? match[ 1 ] : null;
};
const loadYouTubeAPI = () => {
return new Promise( ( resolve ) => {
if ( window.YT && window.YT.loaded ) {
resolve( window.YT );
return;
}
const YOUTUBE_IFRAME_API_URL = 'https://www.youtube.com/iframe_api';
if ( ! document.querySelector( `script[src="${ YOUTUBE_IFRAME_API_URL }"]` ) ) {
const tag = document.createElement( 'script' );
tag.src = YOUTUBE_IFRAME_API_URL;
const firstScriptTag = document.getElementsByTagName( 'script' )[ 0 ];
firstScriptTag.parentNode.insertBefore( tag, firstScriptTag );
}
const checkYT = () => {
if ( window.YT && window.YT.loaded ) {
resolve( window.YT );
} else {
setTimeout( checkYT, 350 );
}
};
checkYT();
} );
};
register( {
elementType: 'e-youtube',
id: 'e-youtube-handler',
callback: ( { element } ) => {
const youtubeElement = document.createElement( 'div' );
youtubeElement.style.height = '100%';
element.appendChild( youtubeElement );
const settingsAttr = element.getAttribute( 'data-settings' );
const parsedSettings = settingsAttr ? JSON.parse( settingsAttr ) : {};
const videoId = getYoutubeVideoIdFromUrl( parsedSettings.source );
if ( ! videoId ) {
return;
}
let player;
let observer;
const prepareYTVideo = ( YT ) => {
const playerOptions = {
videoId,
events: {
onReady: () => {
if ( parsedSettings.mute ) {
player.mute();
}
if ( parsedSettings.autoplay ) {
player.playVideo();
}
},
onStateChange: ( event ) => {
if ( event.data === YT.PlayerState.ENDED && parsedSettings.loop ) {
player.seekTo( parsedSettings.start || 0 );
}
},
},
playerVars: {
controls: parsedSettings.controls ? 1 : 0,
rel: parsedSettings.rel ? 0 : 1,
cc_load_policy: parsedSettings.cc_load_policy ? 1 : 0,
autoplay: parsedSettings.autoplay ? 1 : 0,
start: parsedSettings.start,
end: parsedSettings.end,
},
};
// To handle CORS issues, when the default host is changed, the origin parameter has to be set.
if ( parsedSettings.privacy ) {
playerOptions.host = 'https://www.youtube-nocookie.com';
playerOptions.origin = window.location.hostname;
}
player = new YT.Player( youtubeElement, playerOptions );
return player;
};
if ( parsedSettings.lazyload ) {
observer = new IntersectionObserver(
( entries ) => {
if ( entries[ 0 ].isIntersecting ) {
loadYouTubeAPI().then( ( apiObject ) => prepareYTVideo( apiObject ) );
observer.unobserve( element );
}
},
);
observer.observe( element );
} else {
loadYouTubeAPI().then( ( apiObject ) => prepareYTVideo( apiObject ) );
}
return () => {
if ( player && 'function' === typeof player.destroy ) {
player.destroy();
player = null;
}
if ( element.contains( youtubeElement ) ) {
element.removeChild( youtubeElement );
}
if ( observer && 'function' === typeof observer.disconnect ) {
observer.disconnect();
observer = null;
}
};
},
} );

View File

@@ -0,0 +1,253 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Base;
use Elementor\Element_Base;
use Elementor\Modules\AtomicWidgets\Elements\Loader\Frontend_Assets_Loader;
use Elementor\Modules\AtomicWidgets\PropDependencies\Manager as Dependency_Manager;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Concerns\Has_Meta;
use Elementor\Plugin;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Atomic_Element_Base extends Element_Base {
use Has_Atomic_Base;
use Has_Meta;
protected $version = '0.0';
protected $styles = [];
protected $interactions = [];
protected $editor_settings = [];
public static $widget_description = null;
public function __construct( $data = [], $args = null ) {
parent::__construct( $data, $args );
$this->version = $data['version'] ?? '0.0';
$this->styles = $data['styles'] ?? [];
$this->interactions = $this->parse_atomic_interactions( $data['interactions'] ?? [] );
$this->editor_settings = $data['editor_settings'] ?? [];
$this->add_script_depends( Frontend_Assets_Loader::ATOMIC_WIDGETS_HANDLER );
if ( static::$widget_description ) {
$this->description( static::$widget_description );
}
}
private function parse_atomic_interactions( $interactions ) {
if ( empty( $interactions ) ) {
return [];
}
if ( is_string( $interactions ) ) {
$decoded = json_decode( $interactions, true );
if ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded ) ) {
$interactions = $decoded;
}
}
if ( ! is_array( $interactions ) ) {
return [];
}
return $interactions;
}
private function convert_prop_type_interactions_to_legacy_for_runtime( $interactions ) {
$legacy_items = [];
foreach ( $interactions['items'] as $item ) {
if ( isset( $item['$$type'] ) && 'interaction-item' === $item['$$type'] ) {
$legacy_item = $this->extract_legacy_interaction_from_prop_type( $item );
if ( $legacy_item ) {
$legacy_items[] = $legacy_item;
}
} else {
$legacy_items[] = $item;
}
}
return [
'version' => $interactions['version'] ?? 1,
'items' => $legacy_items,
];
}
abstract protected function define_atomic_controls(): array;
protected function define_atomic_style_states(): array {
return [];
}
public function get_global_scripts() {
return [];
}
final public function get_initial_config() {
$config = parent::get_initial_config();
$props_schema = static::get_props_schema();
$config['atomic_controls'] = $this->get_atomic_controls();
$config['atomic_props_schema'] = $props_schema;
$config['atomic_style_states'] = $this->define_atomic_style_states();
$config['dependencies_per_target_mapping'] = Dependency_Manager::get_source_to_dependents( $props_schema );
$config['base_styles'] = $this->get_base_styles();
$config['version'] = $this->version;
$config['show_in_panel'] = $this->should_show_in_panel();
$config['categories'] = [ 'v4-elements' ];
$config['hide_on_search'] = false;
$config['controls'] = [];
$config['keywords'] = $this->get_keywords();
$config['default_children'] = $this->define_default_children();
$config['initial_attributes'] = $this->define_initial_attributes();
$config['include_in_widgets_config'] = true;
$config['default_html_tag'] = $this->define_default_html_tag();
$config['meta'] = $this->get_meta();
return $config;
}
protected function should_show_in_panel() {
return true;
}
protected function define_default_children() {
return [];
}
protected function define_default_html_tag() {
return 'div';
}
protected function define_initial_attributes() {
return [];
}
protected function add_render_attributes() {
parent::add_render_attributes();
$interaction_ids = $this->get_interactions_ids();
if ( ! empty( $interaction_ids ) ) {
$this->add_render_attribute( '_wrapper', 'data-interaction-id', $this->get_id() );
$this->add_render_attribute( '_wrapper', 'data-interactions', json_encode( $interaction_ids ) );
}
}
/**
* Get Element keywords.
*
* Retrieve the element keywords.
*
* @since 3.29
* @access public
*
* @return array Element keywords.
*/
public function get_keywords() {
return [];
}
/**
* @return array<string, Prop_Type>
*/
abstract protected static function define_props_schema(): array;
/**
* Get the HTML tag for rendering.
*
* @return string
*/
protected function get_html_tag(): string {
$settings = $this->get_atomic_settings();
$default_html_tag = $this->define_default_html_tag();
return ! empty( $settings['link']['href'] ) ? $settings['link']['tag'] : ( $settings['tag'] ?? $default_html_tag );
}
/**
* Print safe HTML tag for the element based on the element settings.
*
* @return void
*/
protected function print_html_tag() {
$html_tag = $this->get_html_tag();
Utils::print_validated_html_tag( $html_tag );
}
/**
* Print custom attributes if they exist.
*
* @return void
*/
protected function print_custom_attributes() {
$settings = $this->get_atomic_settings();
$attributes = $settings['attributes'] ?? '';
if ( ! empty( $attributes ) && is_string( $attributes ) ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo ' ' . $attributes;
}
}
/**
* Get default child type for container elements.
*
* @param array $element_data
* @return mixed
*/
protected function _get_default_child_type( array $element_data ) {
$el_types = array_keys( Plugin::$instance->elements_manager->get_element_types() );
if ( in_array( $element_data['elType'], $el_types, true ) ) {
return Plugin::$instance->elements_manager->get_element_types( $element_data['elType'] );
}
if ( ! isset( $element_data['widgetType'] ) ) {
return null;
}
return Plugin::$instance->widgets_manager->get_widget_types( $element_data['widgetType'] );
}
/**
* Default before render for container elements.
*
* @return void
*/
public function before_render() {
?>
<<?php $this->print_html_tag(); ?> <?php $this->print_render_attribute_string( '_wrapper' );
$this->print_custom_attributes(); ?>>
<?php
}
/**
* Default after render for container elements.
*
* @return void
*/
public function after_render() {
?>
</<?php $this->print_html_tag(); ?>>
<?php
}
/**
* Default content template - can be overridden by elements that need custom templates.
*
* @return void
*/
protected function content_template() {
?>
<?php
}
public static function generate() {
return Element_Builder::make( static::get_type() );
}
}

View File

@@ -0,0 +1,115 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Base;
use Elementor\Modules\AtomicWidgets\Elements\Loader\Frontend_Assets_Loader;
use Elementor\Modules\AtomicWidgets\PropDependencies\Manager as Dependency_Manager;
use Elementor\Modules\AtomicWidgets\PropTypes\Concerns\Has_Meta;
use Elementor\Widget_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
abstract class Atomic_Widget_Base extends Widget_Base {
use Has_Atomic_Base;
use Has_Meta;
public static $widget_description = null;
protected $version = '0.0';
protected $styles = [];
protected $interactions = [];
protected $editor_settings = [];
public function __construct( $data = [], $args = null ) {
parent::__construct( $data, $args );
$this->version = $data['version'] ?? '0.0';
$this->styles = $data['styles'] ?? [];
$this->interactions = $this->parse_atomic_interactions( $data['interactions'] ?? [] );
$this->editor_settings = $data['editor_settings'] ?? [];
if ( static::$widget_description ) {
$this->description( static::$widget_description );
}
}
private function parse_atomic_interactions( $interactions ) {
if ( empty( $interactions ) ) {
return [];
}
if ( is_string( $interactions ) ) {
$decoded = json_decode( $interactions, true );
if ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded ) ) {
$interactions = $decoded;
}
}
if ( ! is_array( $interactions ) ) {
return [];
}
return $interactions;
}
private function convert_prop_type_interactions_to_legacy_for_runtime( $interactions ) {
$legacy_items = [];
foreach ( $interactions['items'] as $item ) {
if ( isset( $item['$$type'] ) && 'interaction-item' === $item['$$type'] ) {
$legacy_item = $this->extract_legacy_interaction_from_prop_type( $item );
if ( $legacy_item ) {
$legacy_items[] = $legacy_item;
}
} else {
$legacy_items[] = $item;
}
}
return [
'version' => $interactions['version'] ?? 1,
'items' => $legacy_items,
];
}
abstract protected function define_atomic_controls(): array;
public function get_global_scripts() {
return [];
}
public function get_initial_config() {
$config = parent::get_initial_config();
$props_schema = static::get_props_schema();
$config['atomic'] = true;
$config['atomic_controls'] = $this->get_atomic_controls();
$config['base_styles'] = $this->get_base_styles();
$config['base_styles_dictionary'] = $this->get_base_styles_dictionary();
$config['atomic_props_schema'] = $props_schema;
$config['dependencies_per_target_mapping'] = Dependency_Manager::get_source_to_dependents( $props_schema );
$config['version'] = $this->version;
$config['meta'] = $this->get_meta();
return $config;
}
public function get_categories(): array {
return [ 'v4-elements' ];
}
public function before_render() {}
public function after_render() {}
abstract protected static function define_props_schema(): array;
public static function generate() {
return Widget_Builder::make( static::get_element_type() );
}
public function get_script_depends() {
return [ Frontend_Assets_Loader::ATOMIC_WIDGETS_HANDLER ];
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Base;
class Element_Builder {
protected $element_type;
protected $settings = [];
protected $is_locked = false;
protected $children = [];
protected $editor_settings = [];
public static function make( string $element_type ) {
return new self( $element_type );
}
private function __construct( string $element_type ) {
$this->element_type = $element_type;
}
public function settings( array $settings ) {
$this->settings = $settings;
return $this;
}
public function is_locked( $is_locked ) {
$this->is_locked = $is_locked;
return $this;
}
public function editor_settings( array $editor_settings ) {
$this->editor_settings = $editor_settings;
return $this;
}
public function children( array $children ) {
$this->children = $children;
return $this;
}
public function build() {
$element_data = [
'elType' => $this->element_type,
'settings' => $this->settings,
'isLocked' => $this->is_locked,
'editor_settings' => $this->editor_settings,
'elements' => $this->children,
];
return $element_data;
}
}

View File

@@ -0,0 +1,483 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Base;
use Elementor\Element_Base;
use Elementor\Modules\AtomicWidgets\Controls\Base\Atomic_Control_Base;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Modules\AtomicWidgets\PropsResolver\Render_Props_Resolver;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Prop_Type;
use Elementor\Modules\AtomicWidgets\Styles\Style_Schema;
use Elementor\Modules\AtomicWidgets\Parsers\Props_Parser;
use Elementor\Modules\AtomicWidgets\Parsers\Style_Parser;
use Elementor\Modules\AtomicWidgets\PropTypes\Attributes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Key_Value_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Link_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
use Elementor\Utils;
use Elementor\Modules\Components\PropTypes\Overridable_Prop_Type;
use Elementor\Modules\AtomicWidgets\Styles\Atomic_Widget_Styles;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* @mixin Element_Base
*/
trait Has_Atomic_Base {
use Has_Base_Styles;
public function has_widget_inner_wrapper(): bool {
return false;
}
abstract public static function get_element_type(): string;
final public function get_name() {
return static::get_element_type();
}
private function get_valid_controls( array $schema, array $controls ): array {
$valid_controls = [];
foreach ( $controls as $control ) {
if ( $control instanceof Section ) {
$cloned_section = clone $control;
$cloned_section->set_items(
$this->get_valid_controls( $schema, $control->get_items() )
);
$valid_controls[] = $cloned_section;
continue;
}
if ( ( $control instanceof Atomic_Control_Base ) ) {
$prop_name = $control->get_bind();
if ( ! $prop_name ) {
Utils::safe_throw( 'Control is missing a bound prop from the schema.' );
continue;
}
if ( ! array_key_exists( $prop_name, $schema ) ) {
Utils::safe_throw( "Prop `{$prop_name}` is not defined in the schema of `{$this->get_name()}`." );
continue;
}
}
$valid_controls[] = $control;
}
return $valid_controls;
}
private static function validate_schema( array $schema ) {
$widget_name = static::class;
foreach ( $schema as $key => $prop ) {
if ( ! ( $prop instanceof Prop_Type ) ) {
Utils::safe_throw( "Prop `$key` must be an instance of `Prop_Type` in `{$widget_name}`." );
}
}
}
private function parse_atomic_styles( array $data ): array {
$styles = $data['styles'] ?? [];
$style_parser = Style_Parser::make( Style_Schema::get() );
foreach ( $styles as $style_id => $style ) {
$result = $style_parser->parse( $style );
if ( ! $result->is_valid() ) {
$widget_id = $data['id'] ?? 'unknown';
throw new \Exception( esc_html( "Styles validation failed for style `$style_id`. Widget ID: `$widget_id`. " . $result->errors()->to_string() ) );
}
$styles[ $style_id ] = $result->unwrap();
}
return $styles;
}
private function parse_atomic_settings( array $settings ): array {
$schema = static::get_props_schema();
$props_parser = Props_Parser::make( $schema );
$result = $props_parser->parse( $settings );
if ( ! $result->is_valid() ) {
throw new \Exception( esc_html( 'Settings validation failed. ' . $result->errors()->to_string() ) );
}
return $result->unwrap();
}
private function parse_atomic_interactions( $interactions ) {
if ( empty( $interactions ) ) {
return [];
}
if ( is_string( $interactions ) ) {
$decoded = json_decode( $interactions, true );
if ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded ) ) {
return $decoded;
}
}
if ( is_array( $interactions ) ) {
return $interactions;
}
return [];
}
private function convert_prop_type_interactions_to_legacy( $interactions ) {
$legacy_items = [];
foreach ( $interactions['items'] as $item ) {
if ( isset( $item['$$type'] ) && 'interaction-item' === $item['$$type'] ) {
$legacy_item = $this->extract_legacy_interaction_from_prop_type( $item );
if ( $legacy_item ) {
$legacy_items[] = $legacy_item;
}
} else {
$legacy_items[] = $item;
}
}
return [
'version' => $interactions['version'] ?? 1,
'items' => $legacy_items,
];
}
private function extract_legacy_interaction_from_prop_type( $item ) {
if ( ! isset( $item['value'] ) || ! is_array( $item['value'] ) ) {
return null;
}
$item_value = $item['value'];
$interaction_id = $this->extract_prop_value( $item_value, 'interaction_id' );
$trigger = $this->extract_prop_value( $item_value, 'trigger' );
$animation = $this->extract_prop_value( $item_value, 'animation' );
if ( ! is_array( $animation ) ) {
return null;
}
$effect = $this->extract_prop_value( $animation, 'effect' );
$type = $this->extract_prop_value( $animation, 'type' );
$direction = $this->extract_prop_value( $animation, 'direction' );
$timing_config = $this->extract_prop_value( $animation, 'timing_config' );
$duration = 300;
$delay = 0;
if ( is_array( $timing_config ) ) {
$duration = $this->extract_prop_value( $timing_config, 'duration', 300 );
$delay = $this->extract_prop_value( $timing_config, 'delay', 0 );
}
$animation_id = implode( '-', [ $trigger, $effect, $type, $direction, $duration, $delay ] );
return [
'interaction_id' => $interaction_id,
'animation' => [
'animation_id' => $animation_id,
'animation_type' => 'full-preset',
],
];
}
private function extract_prop_value( $data, $key, $default = '' ) {
if ( ! is_array( $data ) || ! isset( $data[ $key ] ) ) {
return $default;
}
$value = $data[ $key ];
if ( is_array( $value ) && isset( $value['$$type'] ) && isset( $value['value'] ) ) {
return $value['value'];
}
return null !== $value ? $value : $default;
}
public function get_atomic_controls() {
$controls = apply_filters(
'elementor/atomic-widgets/controls',
$this->define_atomic_controls(),
$this
);
$schema = static::get_props_schema();
// Validate the schema only in the Editor.
static::validate_schema( $schema );
return $this->get_valid_controls( $schema, $controls );
}
protected function get_css_id_control_meta(): array {
return [
'layout' => 'two-columns',
'topDivider' => true,
];
}
final public function get_controls( $control_id = null ) {
if ( ! empty( $control_id ) ) {
return null;
}
return [];
}
final public function get_data_for_save() {
$data = parent::get_data_for_save();
$data['version'] = $this->version;
$data['settings'] = $this->parse_atomic_settings( $data['settings'] );
$data['styles'] = $this->parse_atomic_styles( $data );
$data['editor_settings'] = $this->parse_editor_settings( $data['editor_settings'] );
if ( isset( $data['interactions'] ) && ! empty( $data['interactions'] ) ) {
$data['interactions'] = $this->transform_interactions_for_save( $data['interactions'] );
} else {
$data['interactions'] = [];
}
return $data;
}
private function transform_interactions_for_save( $interactions ) {
$decoded = $this->decode_interactions_data( $interactions );
if ( empty( $decoded['items'] ) ) {
return [];
}
return $decoded;
}
private function decode_interactions_data( $interactions ) {
if ( is_array( $interactions ) ) {
return $interactions;
}
if ( is_string( $interactions ) ) {
$decoded = json_decode( $interactions, true );
if ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded ) ) {
return $decoded;
}
}
return [
'items' => [],
'version' => 1,
];
}
final public function get_raw_data( $with_html_content = false ) {
$raw_data = parent::get_raw_data( $with_html_content );
$raw_data['styles'] = Atomic_Widget_Styles::get_license_based_filtered_styles( $this->styles ?? [] );
$raw_data['interactions'] = $this->interactions ?? [];
$raw_data['editor_settings'] = $this->editor_settings;
return $raw_data;
}
final public function get_stack( $with_common_controls = true ) {
return [
'controls' => [],
'tabs' => [],
];
}
public function get_atomic_settings(): array {
$schema = static::get_props_schema();
$props = $this->get_settings();
$initial_attributes = $this->get_initial_attributes();
$props['attributes'] = Attributes_Prop_Type::generate( array_merge(
$initial_attributes['value'] ?? [],
$props['attributes']['value'] ?? []
) );
return Render_Props_Resolver::for_settings()->resolve( $schema, $props );
}
protected function get_initial_attributes() {
return Attributes_Prop_Type::generate( [
Key_Value_Prop_Type::generate( [
'key' => String_Prop_Type::generate( 'data-e-type' ),
'value' => $this->get_type(),
] ),
Key_Value_Prop_Type::generate( [
'key' => String_Prop_Type::generate( 'data-id' ),
'value' => $this->get_id(),
] ),
] );
}
public function get_atomic_setting( string $key ) {
$schema = static::get_props_schema();
if ( ! isset( $schema[ $key ] ) ) {
return null;
}
$props = $this->get_settings();
$prop_value = $props[ $key ] ?? null;
$single_schema = [ $key => $schema[ $key ] ];
$single_props = [ $key => $prop_value ];
$resolved = Render_Props_Resolver::for_settings()->resolve( $single_schema, $single_props );
return $resolved[ $key ] ?? null;
}
protected function parse_editor_settings( array $data ): array {
$editor_data = [];
if ( isset( $data['title'] ) && is_string( $data['title'] ) ) {
$editor_data['title'] = sanitize_text_field( $data['title'] );
}
return $editor_data;
}
public static function get_props_schema(): array {
$schema = static::define_props_schema();
$schema['_cssid'] = String_Prop_Type::make()->meta( Overridable_Prop_Type::ignore() );
return apply_filters(
'elementor/atomic-widgets/props-schema',
$schema
);
}
public function get_interactions_ids() {
$animation_ids = [];
$list_of_interactions = ( is_array( $this->interactions ) && isset( $this->interactions['items'] ) )
? $this->interactions['items']
: [];
foreach ( $list_of_interactions as $interaction ) {
if ( isset( $interaction['$$type'] ) && 'interaction-item' === $interaction['$$type'] ) {
$animation_id = $this->extract_animation_id_from_prop_type( $interaction );
if ( $animation_id ) {
$animation_ids[] = $animation_id;
}
} elseif ( isset( $interaction['animation']['animation_id'] ) ) {
$animation_ids[] = $interaction['animation']['animation_id'];
}
}
return $animation_ids;
}
private function extract_animation_id_from_prop_type( $item ) {
if ( ! isset( $item['value'] ) || ! is_array( $item['value'] ) ) {
return null;
}
$item_value = $item['value'];
$trigger = $this->extract_prop_value( $item_value, 'trigger' );
$animation = $this->extract_prop_value( $item_value, 'animation' );
if ( ! is_array( $animation ) ) {
return null;
}
$effect = $this->extract_prop_value( $animation, 'effect' );
$type = $this->extract_prop_value( $animation, 'type' );
$direction = $this->extract_prop_value( $animation, 'direction' );
$timing_config = $this->extract_prop_value( $animation, 'timing_config' );
$config = $this->extract_prop_value( $animation, 'config' );
$duration = 300;
$delay = 0;
$replay = 0;
$relative_to = 'viewport';
$offset_top = 15;
$offset_bottom = 85;
if ( is_array( $timing_config ) ) {
$duration = $this->extract_prop_value( $timing_config, 'duration', 300 );
$delay = $this->extract_prop_value( $timing_config, 'delay', 0 );
}
if ( is_array( $config ) ) {
$relative_to = $this->extract_prop_value( $config, 'relative_to', 'viewport' );
$offset_top = $this->extract_prop_value( $config, 'offset_top', 15 );
$offset_bottom = $this->extract_prop_value( $config, 'offset_bottom', 85 );
$replay = $this->extract_prop_value( $config, 'replay', 0 );
if ( empty( $replay ) && 0 !== $replay && '0' !== $replay ) {
$replay = 0;
}
} else {
$replay = 0;
}
return implode( '-', [ $trigger, $effect, $type, $direction, $duration, $delay, $replay, $relative_to, $offset_top, $offset_bottom ] );
}
public function print_content() {
$defined_context = $this->define_render_context();
$context_key = $defined_context['context_key'] ?? static::class;
$element_context = $defined_context['context'] ?? [];
$has_context = ! empty( $element_context );
if ( ! $has_context ) {
return parent::print_content();
}
Render_Context::push( $context_key, $element_context );
parent::print_content();
Render_Context::pop( $context_key );
}
/**
* Define the context for element's Render_Context.
*
* @return array{context_key: ?string, context: array}
*/
protected function define_render_context(): array {
return [
'context_key' => null,
'context' => [],
];
}
protected function get_link_attributes( $link_settings, $add_key_to_result = false ) {
$tag = $link_settings['tag'] ?? Link_Prop_Type::DEFAULT_TAG;
$href = $link_settings['href'];
$target = $link_settings['target'] ?? '_self';
$is_button = 'button' === $tag;
$href_attribute_key = $is_button ? 'data-action-link' : 'href';
$result = [
$href_attribute_key => $href,
'target' => $target,
];
if ( $add_key_to_result ) {
$result['key'] = $href_attribute_key;
}
return $result;
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Base;
use Elementor\Core\Utils\Collection;
use Elementor\Modules\AtomicWidgets\Styles\Style_Definition;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* @mixin Has_Atomic_Base
*/
trait Has_Base_Styles {
public function get_base_styles() {
$base_styles = $this->define_base_styles();
$style_definitions = [];
foreach ( $base_styles as $key => $style ) {
$id = $this->generate_base_style_id( $key );
$style_definitions[ $id ] = $style->build( $id );
}
return $style_definitions;
}
public function get_base_styles_dictionary() {
$result = [];
$base_styles = array_keys( $this->define_base_styles() );
foreach ( $base_styles as $key ) {
$result[ $key ] = $this->generate_base_style_id( $key );
}
return $result;
}
private function generate_base_style_id( string $key ): string {
return static::get_element_type() . '-' . $key;
}
/**
* @return array<string, Style_Definition>
*/
protected function define_base_styles(): array {
return [];
}
}

View File

@@ -0,0 +1,161 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Base;
use Elementor\Modules\AtomicWidgets\Elements\TemplateRenderer\Template_Renderer;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* @mixin Has_Atomic_Base
*/
trait Has_Template {
public function get_initial_config() {
$config = parent::get_initial_config();
$config['twig_main_template'] = $this->get_main_template();
$config['twig_templates'] = $this->get_templates_contents();
return $config;
}
protected function render() {
try {
$renderer = Template_Renderer::instance();
foreach ( $this->get_templates() as $name => $path ) {
if ( $renderer->is_registered( $name ) ) {
continue;
}
$renderer->register( $name, $path );
}
$context = [
'id' => $this->get_id(),
'type' => $this->get_name(),
'settings' => $this->get_atomic_settings(),
'base_styles' => $this->get_base_styles_dictionary(),
'interactions' => $this->get_interactions_ids(),
];
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $renderer->render( $this->get_main_template(), $context );
} catch ( \Exception $e ) {
if ( Utils::is_elementor_debug() ) {
throw $e;
}
}
}
public function get_interactions_ids() {
$animation_ids = [];
$list_of_interactions = ( is_array( $this->interactions ) && isset( $this->interactions['items'] ) )
? $this->interactions['items']
: [];
foreach ( $list_of_interactions as $interaction ) {
if ( isset( $interaction['$$type'] ) && 'interaction-item' === $interaction['$$type'] ) {
$animation_id = $this->extract_animation_id_from_prop_type( $interaction );
if ( $animation_id ) {
$animation_ids[] = $animation_id;
}
} elseif ( isset( $interaction['animation']['animation_id'] ) ) {
$animation_ids[] = $interaction['animation']['animation_id'];
}
}
return $animation_ids;
}
private function extract_animation_id_from_prop_type( $item ) {
if ( ! isset( $item['value'] ) || ! is_array( $item['value'] ) ) {
return null;
}
$item_value = $item['value'];
$trigger = $this->extract_prop_value_simple( $item_value, 'trigger' );
$animation = $this->extract_prop_value_simple( $item_value, 'animation' );
if ( ! is_array( $animation ) ) {
return null;
}
$effect = $this->extract_prop_value_simple( $animation, 'effect' );
$type = $this->extract_prop_value_simple( $animation, 'type' );
$direction = $this->extract_prop_value_simple( $animation, 'direction' );
$timing_config = $this->extract_prop_value_simple( $animation, 'timing_config' );
$config = $this->extract_prop_value_simple( $animation, 'config' );
$duration = 300;
$delay = 0;
$replay = 0;
$relative_to = 'viewport';
$offset_top = 15;
$offset_bottom = 85;
if ( is_array( $timing_config ) ) {
$duration = $this->extract_prop_value_simple( $timing_config, 'duration', 300 );
$delay = $this->extract_prop_value_simple( $timing_config, 'delay', 0 );
}
if ( is_array( $config ) ) {
$relative_to = $this->extract_prop_value_simple( $config, 'relative_to', 'viewport' );
$offset_top = $this->extract_prop_value_simple( $config, 'offset_top', 15 );
$offset_bottom = $this->extract_prop_value_simple( $config, 'offset_bottom', 85 );
$replay = $this->extract_prop_value_simple( $config, 'replay', 0 );
if ( empty( $replay ) && 0 !== $replay && '0' !== $replay ) {
$replay = 0;
}
} else {
$replay = 0;
}
return implode( '-', [ $trigger, $effect, $type, $direction, $duration, $delay, $replay, $relative_to, $offset_top, $offset_bottom ] );
}
private function extract_prop_value_simple( $data, $key, $default = '' ) {
if ( ! is_array( $data ) || ! isset( $data[ $key ] ) ) {
return $default;
}
$value = $data[ $key ];
if ( is_array( $value ) && isset( $value['$$type'] ) && isset( $value['value'] ) ) {
return $value['value'];
}
return null !== $value ? $value : $default;
}
protected function get_templates_contents() {
return array_map(
fn ( $path ) => Utils::file_get_contents( $path ),
$this->get_templates()
);
}
protected function get_main_template() {
$templates = $this->get_templates();
if ( count( $templates ) > 1 ) {
Utils::safe_throw( 'When having more than one template, you should override this method to return the main template.' );
return null;
}
foreach ( $templates as $key => $path ) {
// Returns first key in the array.
return $key;
}
return null;
}
abstract protected function get_templates(): array;
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Render_Context {
private static $context_stack = [];
public static function push( string $key, array $context ): void {
if ( ! self::get( $key ) ) {
self::$context_stack[ $key ] = [];
}
self::$context_stack[ $key ][] = $context;
}
public static function pop( string $key ): void {
if ( isset( self::$context_stack[ $key ] ) && ! empty( self::$context_stack[ $key ] ) ) {
array_pop( self::$context_stack[ $key ] );
}
}
public static function get( string $key ): array {
if ( ! isset( self::$context_stack[ $key ] ) || empty( self::$context_stack[ $key ] ) ) {
return [];
}
$last_key = array_key_last( self::$context_stack[ $key ] );
return self::$context_stack[ $key ][ $last_key ];
}
public static function clear(): void {
self::$context_stack = [];
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Base;
class Widget_Builder {
protected $widget_type;
protected $settings = [];
protected $is_locked = false;
protected $editor_settings = [];
public static function make( string $widget_type ) {
return new self( $widget_type );
}
private function __construct( string $widget_type ) {
$this->widget_type = $widget_type;
}
public function settings( array $settings ) {
$this->settings = $settings;
return $this;
}
public function is_locked( $is_locked ) {
$this->is_locked = $is_locked;
return $this;
}
public function editor_settings( array $editor_settings ) {
$this->editor_settings = $editor_settings;
return $this;
}
public function build() {
$widget_data = [
'elType' => 'widget',
'widgetType' => $this->widget_type,
'settings' => $this->settings,
'isLocked' => $this->is_locked,
'editor_settings' => $this->editor_settings,
];
return $widget_data;
}
}

View File

@@ -0,0 +1,187 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Div_Block;
use Elementor\Modules\AtomicWidgets\Elements\Base\Atomic_Element_Base;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Size_Prop_Type;
use Elementor\Modules\AtomicWidgets\Styles\Style_Definition;
use Elementor\Modules\AtomicWidgets\PropTypes\Attributes_Prop_Type;
use Elementor\Modules\AtomicWidgets\Styles\Style_Variant;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Modules\AtomicWidgets\Controls\Types\Link_Control;
use Elementor\Modules\AtomicWidgets\Controls\Types\Html_Tag_Control;
use Elementor\Modules\AtomicWidgets\Controls\Types\Text_Control;
use Elementor\Modules\AtomicWidgets\PropDependencies\Manager as Dependency_Manager;
use Elementor\Modules\AtomicWidgets\PropTypes\Classes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Link_Prop_Type;
use Elementor\Modules\Components\PropTypes\Overridable_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Div_Block extends Atomic_Element_Base {
const BASE_STYLE_KEY = 'base';
public function __construct( $data = [], $args = null ) {
parent::__construct( $data, $args );
$this->meta( 'is_container', true );
}
public static function get_type() {
return 'e-div-block';
}
public static function get_element_type(): string {
return 'e-div-block';
}
public function get_title() {
return esc_html__( 'Div block', 'elementor' );
}
public function get_keywords() {
return [ 'ato', 'atom', 'atoms', 'atomic' ];
}
public function get_icon() {
return 'eicon-div-block';
}
protected static function define_props_schema(): array {
$tag_dependencies = Dependency_Manager::make( Dependency_Manager::RELATION_AND )
->where( [
'operator' => 'ne',
'path' => [ 'link', 'destination' ],
'nestedPath' => [ 'group' ],
'value' => 'action',
'newValue' => [
'$$type' => 'string',
'value' => 'button',
],
] )->where( [
'operator' => 'not_exist',
'path' => [ 'link', 'destination' ],
'newValue' => [
'$$type' => 'string',
'value' => 'a',
],
] )->get();
return [
'classes' => Classes_Prop_Type::make()
->default( [] ),
'tag' => String_Prop_Type::make()
->enum( [ 'div', 'header', 'section', 'article', 'aside', 'footer', 'a', 'button' ] )
->default( 'div' )
->set_dependencies( $tag_dependencies ),
'link' => Link_Prop_Type::make(),
'attributes' => Attributes_Prop_Type::make()->meta( Overridable_Prop_Type::ignore() ),
];
}
protected function define_atomic_controls(): array {
return [
Section::make()
->set_label( __( 'Settings', 'elementor' ) )
->set_id( 'settings' )
->set_items( [
Html_Tag_Control::bind_to( 'tag' )
->set_options( [
[
'value' => 'div',
'label' => 'Div',
],
[
'value' => 'header',
'label' => 'Header',
],
[
'value' => 'section',
'label' => 'Section',
],
[
'value' => 'article',
'label' => 'Article',
],
[
'value' => 'aside',
'label' => 'Aside',
],
[
'value' => 'footer',
'label' => 'Footer',
],
])
->set_fallback_labels( [
'a' => 'a (link)',
] )
->set_label( esc_html__( 'HTML Tag', 'elementor' ) ),
Link_Control::bind_to( 'link' )
->set_placeholder( __( 'Type or paste your URL', 'elementor' ) )
->set_label( __( 'Link', 'elementor' ) )
->set_meta( [
'topDivider' => true,
] ),
Text_Control::bind_to( '_cssid' )
->set_label( __( 'ID', 'elementor' ) )
->set_meta( $this->get_css_id_control_meta() ),
] ),
];
}
protected function define_base_styles(): array {
$display = String_Prop_Type::generate( 'block' );
return [
static::BASE_STYLE_KEY => Style_Definition::make()
->add_variant(
Style_Variant::make()
->add_prop( 'display', $display )
->add_prop( 'padding', $this->get_base_padding() )
->add_prop( 'min-width', $this->get_base_min_width() )
),
];
}
protected function get_base_padding(): array {
return Size_Prop_Type::generate( [
'size' => 10,
'unit' => 'px',
] );
}
protected function get_base_min_width(): array {
return Size_Prop_Type::generate( [
'size' => 30,
'unit' => 'px',
] );
}
protected function add_render_attributes() {
parent::add_render_attributes();
$settings = $this->get_atomic_settings();
$base_style_class = $this->get_base_styles_dictionary()[ static::BASE_STYLE_KEY ];
$initial_attributes = $this->define_initial_attributes();
$attributes = [
'class' => [
'e-con',
'e-atomic-element',
$base_style_class,
...( $settings['classes'] ?? [] ),
],
];
if ( ! empty( $settings['_cssid'] ) ) {
$attributes['id'] = esc_attr( $settings['_cssid'] );
}
if ( ! empty( $settings['link']['href'] ) ) {
$link_attributes = $this->get_link_attributes( $settings['link'] );
$attributes = array_merge( $attributes, $link_attributes );
}
$this->add_render_attribute( '_wrapper', array_merge( $initial_attributes, $attributes ) );
}
}

View File

@@ -0,0 +1,184 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Flexbox;
use Elementor\Modules\AtomicWidgets\Elements\Base\Atomic_Element_Base;
use Elementor\Modules\AtomicWidgets\PropDependencies\Manager as Dependency_Manager;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Size_Prop_Type;
use Elementor\Modules\AtomicWidgets\Styles\Style_Definition;
use Elementor\Modules\AtomicWidgets\Styles\Style_Variant;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Modules\AtomicWidgets\Controls\Types\Html_Tag_Control;
use Elementor\Modules\AtomicWidgets\Controls\Types\Link_Control;
use Elementor\Modules\AtomicWidgets\Controls\Types\Text_Control;
use Elementor\Modules\AtomicWidgets\PropTypes\Attributes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Classes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Link_Prop_Type;
use Elementor\Modules\Components\PropTypes\Overridable_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Flexbox extends Atomic_Element_Base {
const BASE_STYLE_KEY = 'base';
public function __construct( $data = [], $args = null ) {
parent::__construct( $data, $args );
$this->meta( 'is_container', true );
}
public static function get_type() {
return 'e-flexbox';
}
public static function get_element_type(): string {
return 'e-flexbox';
}
public function get_title() {
return esc_html__( 'Flexbox', 'elementor' );
}
public function get_keywords() {
return [ 'ato', 'atom', 'atoms', 'atomic' ];
}
public function get_icon() {
return 'eicon-flexbox';
}
protected static function define_props_schema(): array {
$tag_dependencies = Dependency_Manager::make( Dependency_Manager::RELATION_AND )
->where( [
'operator' => 'ne',
'path' => [ 'link', 'destination' ],
'nestedPath' => [ 'group' ],
'value' => 'action',
'newValue' => [
'$$type' => 'string',
'value' => 'button',
],
] )->where( [
'operator' => 'not_exist',
'path' => [ 'link', 'destination' ],
'newValue' => [
'$$type' => 'string',
'value' => 'a',
],
] )->get();
return [
'classes' => Classes_Prop_Type::make()
->default( [] ),
'tag' => String_Prop_Type::make()
->enum( [ 'div', 'header', 'section', 'article', 'aside', 'footer', 'a', 'button' ] )
->default( 'div' )
->description( 'The HTML tag for the flexbox container. Could be div, header, section, article, aside, footer, or a (link).' )
->set_dependencies( $tag_dependencies ),
'link' => Link_Prop_Type::make(),
'attributes' => Attributes_Prop_Type::make()->meta( Overridable_Prop_Type::ignore() ),
];
return $schema;
}
protected function define_atomic_controls(): array {
return [
Section::make()
->set_label( __( 'Settings', 'elementor' ) )
->set_id( 'settings' )
->set_items( [
Html_Tag_Control::bind_to( 'tag' )
->set_options( [
[
'value' => 'div',
'label' => 'Div',
],
[
'value' => 'header',
'label' => 'Header',
],
[
'value' => 'section',
'label' => 'Section',
],
[
'value' => 'article',
'label' => 'Article',
],
[
'value' => 'aside',
'label' => 'Aside',
],
[
'value' => 'footer',
'label' => 'Footer',
],
])
->set_label( esc_html__( 'HTML Tag', 'elementor' ) )
->set_fallback_labels( [
'a' => 'a (link)',
] ),
Link_Control::bind_to( 'link' )
->set_placeholder( __( 'Type or paste your URL', 'elementor' ) )
->set_label( __( 'Link', 'elementor' ) )
->set_meta( [
'topDivider' => true,
] ),
Text_Control::bind_to( '_cssid' )
->set_label( __( 'ID', 'elementor' ) )
->set_meta( $this->get_css_id_control_meta() ),
] ),
];
}
protected function define_base_styles(): array {
$display = String_Prop_Type::generate( 'flex' );
$flex_direction = String_Prop_Type::generate( 'row' );
return [
static::BASE_STYLE_KEY => Style_Definition::make()
->add_variant(
Style_Variant::make()
->add_prop( 'display', $display )
->add_prop( 'flex-direction', $flex_direction )
->add_prop( 'padding', $this->get_base_padding() )
),
];
}
protected function get_base_padding(): array {
return Size_Prop_Type::generate( [
'size' => 10,
'unit' => 'px',
] );
}
protected function add_render_attributes() {
parent::add_render_attributes();
$settings = $this->get_atomic_settings();
$base_style_class = $this->get_base_styles_dictionary()[ static::BASE_STYLE_KEY ];
$initial_attributes = $this->define_initial_attributes();
$attributes = [
'class' => [
'e-con',
'e-atomic-element',
$base_style_class,
...( $settings['classes'] ?? [] ),
],
];
if ( ! empty( $settings['_cssid'] ) ) {
$attributes['id'] = esc_attr( $settings['_cssid'] );
}
if ( ! empty( $settings['link']['href'] ) ) {
$link_attributes = $this->get_link_attributes( $settings['link'] );
$attributes = array_merge( $attributes, $link_attributes );
}
$this->add_render_attribute( '_wrapper', array_merge( $initial_attributes, $attributes ) );
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Loader;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Frontend_Assets_Loader {
const ALPINEJS_HANDLE = 'elementor-v2-alpinejs';
const FRONTEND_HANDLERS_HANDLE = 'elementor-v2-frontend-handlers';
const ATOMIC_WIDGETS_HANDLER = 'elementor-v2-widgets-frontend';
/**
* @return void
*/
public function register_scripts() {
$this->register_package_scripts();
do_action( 'elementor/atomic-widgets/frontend/loader/scripts/register', $this );
}
private function register_package_scripts() {
$assets_url = ELEMENTOR_ASSETS_URL;
$min_suffix = ( Utils::is_script_debug() || Utils::is_elementor_tests() ) ? '' : '.min';
wp_register_script(
self::FRONTEND_HANDLERS_HANDLE,
"{$assets_url}js/packages/frontend-handlers/frontend-handlers{$min_suffix}.js",
[ 'jquery' ],
ELEMENTOR_VERSION,
true
);
wp_register_script(
self::ALPINEJS_HANDLE,
"{$assets_url}js/packages/alpinejs/alpinejs{$min_suffix}.js",
[],
ELEMENTOR_VERSION,
true
);
wp_register_script(
self::ATOMIC_WIDGETS_HANDLER,
"{$assets_url}js/atomic-widgets-frontend-handler{$min_suffix}.js",
[ self::FRONTEND_HANDLERS_HANDLE ],
ELEMENTOR_VERSION,
true
);
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\TemplateRenderer;
use ElementorDeps\Twig\Error\LoaderError;
use ElementorDeps\Twig\Loader\LoaderInterface;
use ElementorDeps\Twig\Source;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Single_File_Loader implements LoaderInterface {
private $templates = [];
private $validity_cache = [];
public function getSourceContext( string $name ): Source {
$path = $this->get_template_path( $name );
return new Source(
// This is safe to use because we're validating the file path inside `get_template_path`.
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
file_get_contents( $path ),
$name,
$path
);
}
public function getCacheKey( string $name ): string {
return $this->get_template_path( $name );
}
public function isFresh( string $name, int $time ): bool {
$path = $this->get_template_path( $name );
return filemtime( $path ) < $time;
}
public function exists( string $name ) {
$path = $this->templates[ $name ] ?? null;
return $this->is_valid_file( $path );
}
public function is_registered( string $name ): bool {
return isset( $this->templates[ $name ] );
}
public function register( string $name, string $path ): self {
if ( ! $this->is_valid_file( $path ) ) {
throw new LoaderError( esc_html( "Invalid template '{$name}': {$path}" ) );
}
$this->templates[ $name ] = $path;
return $this;
}
private function get_template_path( string $name ): string {
$path = $this->templates[ $name ] ?? null;
if ( ! $this->is_valid_file( $path ) ) {
throw new LoaderError( esc_html( "Invalid template '{$name}': {$path}" ) );
}
return $path;
}
private function is_valid_file( $path ): bool {
if ( ! $path ) {
return false;
}
if ( isset( $this->validity_cache[ $path ] ) ) {
return $this->validity_cache[ $path ];
}
// Ref: https://github.com/twigphp/Twig/blob/8432946eeeca009d75fc7fc568f3c3f4650f5a0f/src/Loader/FilesystemLoader.php#L260
if ( str_contains( $path, "\0" ) ) {
throw new LoaderError( 'A template name cannot contain NULL bytes.' );
}
$is_valid = is_file( $path ) && is_readable( $path );
$this->validity_cache[ $path ] = $is_valid;
return $is_valid;
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\TemplateRenderer;
use Elementor\Utils;
use ElementorDeps\Twig\Environment;
use ElementorDeps\Twig\Runtime\EscaperRuntime;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Template_Renderer {
private static ?self $instance = null;
private Single_File_Loader $loader;
private Environment $env;
private function __construct() {
$this->loader = new Single_File_Loader();
$this->env = new Environment(
$this->loader,
[
'debug' => Utils::is_elementor_debug(),
'autoescape' => 'name',
]
);
$escaper = $this->env->getRuntime( EscaperRuntime::class );
$escaper->setEscaper( 'full_url', 'esc_url' );
$escaper->setEscaper( 'html_tag', [ Utils::class, 'validate_html_tag' ] );
}
public static function instance(): self {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
public static function reset() {
self::$instance = null;
}
public function is_registered( string $name ): bool {
return $this->loader->is_registered( $name );
}
public function register( string $name, string $path ): self {
$this->loader->register( $name, $path );
return $this;
}
public function render( string $name, array $context = [] ): string {
return $this->env->render( $name, $context );
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Elementor\Modules\AtomicWidgets\ImportExport;
use Elementor\Modules\AtomicWidgets\Elements\Base\Atomic_Element_Base;
use Elementor\Modules\AtomicWidgets\Elements\Base\Atomic_Widget_Base;
use Elementor\Modules\AtomicWidgets\ImportExport\Modifiers\Settings_Props_Modifier;
use Elementor\Modules\AtomicWidgets\ImportExport\Modifiers\Styles_Ids_Modifier;
use Elementor\Modules\AtomicWidgets\ImportExport\Modifiers\Styles_Props_Modifier;
use Elementor\Modules\AtomicWidgets\PropsResolver\Import_Export_Props_Resolver;
use Elementor\Modules\AtomicWidgets\Styles\Style_Schema;
use Elementor\Modules\AtomicWidgets\Utils\Utils;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Atomic_Import_Export {
public function register_hooks() {
add_filter(
'elementor/template_library/sources/local/import/elements',
fn( $elements ) => $this->run( $elements, Import_Export_Props_Resolver::for_import() )
);
add_filter(
'elementor/template_library/sources/cloud/import/elements',
fn( $elements ) => $this->run( $elements, Import_Export_Props_Resolver::for_import() )
);
add_filter(
'elementor/template_library/sources/local/export/elements',
fn( $elements ) => $this->run( $elements, Import_Export_Props_Resolver::for_export() )
);
add_filter(
'elementor/document/element/replace_id',
fn( $element ) => $this->replace_styles_ids( $element )
);
}
private function run( $elements, Import_Export_Props_Resolver $props_resolver ) {
if ( empty( $elements ) || ! is_array( $elements ) ) {
return $elements;
}
return Plugin::$instance->db->iterate_data( $elements, function ( $element ) use ( $props_resolver ) {
$element_instance = Plugin::$instance->elements_manager->create_element_instance( $element );
/** @var Atomic_Element_Base | Atomic_Widget_Base $element_instance */
if ( ! Utils::is_atomic( $element_instance ) ) {
return $element;
}
$runners = [
Settings_Props_Modifier::make( $props_resolver, $element_instance::get_props_schema() ),
Styles_Props_Modifier::make( $props_resolver, Style_Schema::get() ),
];
foreach ( $runners as $runner ) {
$element = $runner->run( $element );
}
return $element;
} );
}
private function replace_styles_ids( $element ) {
if ( empty( $element ) || ! is_array( $element ) ) {
return $element;
}
$element_instance = Plugin::$instance->elements_manager->create_element_instance( $element );
/** @var Atomic_Element_Base | Atomic_Widget_Base $element_instance */
if ( ! Utils::is_atomic( $element_instance ) ) {
return $element;
}
return Styles_Ids_Modifier::make()->run( $element );
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Elementor\Modules\AtomicWidgets\ImportExport\Modifiers;
use Elementor\Modules\AtomicWidgets\PropsResolver\Import_Export_Props_Resolver;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Settings_Props_Modifier {
private Import_Export_Props_Resolver $props_resolver;
private array $schema;
public function __construct( Import_Export_Props_Resolver $props_resolver, array $schema ) {
$this->props_resolver = $props_resolver;
$this->schema = $schema;
}
public static function make( Import_Export_Props_Resolver $props_resolver, array $schema ) {
return new self( $props_resolver, $schema );
}
public function run( array $element ) {
if ( empty( $element['settings'] ) || ! is_array( $element['settings'] ) ) {
return $element;
}
$element['settings'] = $this->props_resolver->resolve(
$this->schema,
$element['settings']
);
return $element;
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Elementor\Modules\AtomicWidgets\ImportExport\Modifiers;
use Elementor\Core\Utils\Collection;
use Elementor\Modules\AtomicWidgets\PropTypes\Classes_Prop_Type;
use Elementor\Modules\AtomicWidgets\Utils\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Styles_Ids_Modifier {
private Collection $old_to_new_ids;
public static function make() {
return new self();
}
public function run( array $element ) {
$this->old_to_new_ids = Collection::make();
$element = $this->replace_styles_ids( $element );
$element = $this->replace_references( $element );
return $element;
}
private function replace_styles_ids( array $element ) {
if ( empty( $element['styles'] ) || empty( $element['id'] ) ) {
return $element;
}
$styles = Collection::make( $element['styles'] )->map_with_keys( function ( $style, $id ) use ( $element ) {
$style['id'] = $this->generate_id( $element['id'], $id );
return [ $style['id'] => $style ];
} )->all();
$element['styles'] = $styles;
return $element;
}
private function replace_references( array $element ) {
if ( empty( $element['settings'] ) ) {
return $element;
}
$element['settings'] = Collection::make( $element['settings'] )->map( function ( $setting ) {
if ( ! $setting || ! Classes_Prop_Type::make()->validate( $setting ) ) {
return $setting;
}
$setting['value'] = Collection::make( $setting['value'] )
->map( fn( $style_id ) => $this->old_to_new_ids->get( $style_id ) ?? $style_id )
->all();
return $setting;
} )->all();
return $element;
}
private function generate_id( $element_id, $old_id ): string {
$id = Utils::generate_id( "e-{$element_id}-", $this->old_to_new_ids->values() );
$this->old_to_new_ids[ $old_id ] = $id;
return $id;
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Elementor\Modules\AtomicWidgets\ImportExport\Modifiers;
use Elementor\Modules\AtomicWidgets\PropsResolver\Import_Export_Props_Resolver;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Styles_Props_Modifier {
private Import_Export_Props_Resolver $props_resolver;
private array $schema;
public function __construct( Import_Export_Props_Resolver $props_resolver, array $schema ) {
$this->props_resolver = $props_resolver;
$this->schema = $schema;
}
public static function make( Import_Export_Props_Resolver $props_resolver, array $schema ) {
return new self( $props_resolver, $schema );
}
public function run( array $element ) {
if ( empty( $element['styles'] ) && ! is_array( $element['styles'] ) ) {
return $element;
}
foreach ( $element['styles'] as $style_key => $style ) {
if ( empty( $style['variants'] ) || ! is_array( $style['variants'] ) ) {
continue;
}
foreach ( $style['variants'] as $variant_key => $variant ) {
if ( empty( $variant['props'] ) || ! is_array( $variant['props'] ) ) {
continue;
}
$element['styles'][ $style_key ]['variants'][ $variant_key ]['props'] = $this->props_resolver->resolve(
$this->schema,
$variant['props']
);
}
}
return $element;
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Library;
use Elementor\Plugin;
class Atomic_Widgets_Library {
public function register_hooks() {
add_action( 'elementor/documents/register', fn() => $this->register_documents() );
}
public function register_documents() {
Plugin::$instance->documents
->register_document_type( 'e-div-block', Div_Block::get_class_full_name() )
->register_document_type( 'e-flexbox', Flexbox::get_class_full_name() );
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Library;
use Elementor\Modules\Library\Documents\Library_Document;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor Div_Block library document.
*
* Elementor div block library document handler class is responsible for
* handling a document of a div block type.
*
* @since 3.29.0
*/
class Div_Block 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 'e-div-block';
}
/**
* 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__( 'Div Block', 'elementor' );
}
/**
* Get Type
*
* Return the div block document type.
*
* @return string
*/
public static function get_type() {
return 'e-div-block';
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Library;
use Elementor\Modules\Library\Documents\Library_Document;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor Flexbox library document.
*
* Elementor flexbox library document handler class is responsible for
* handling a document of a flexbox type.
*
* @since 3.29.0
*/
class Flexbox 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 'e-flexbox';
}
/**
* 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__( 'Flexbox', 'elementor' );
}
/**
* Get Type
*
* Return the flexbox document type.
*
* @return string
*/
public static function get_type() {
return 'e-flexbox';
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Logger;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Simple logger for Atomic Widgets that writes to wp-content/debug.log (if WP_DEBUG_LOG enabled
* and WP_DEBUG_DISPLAY disabled) or optionally to Elementor's DB logger.
* Never displays errors on screen.
*/
class Logger {
public static function info( string $message, array $context = [], bool $use_elementor_logger = false ): void {
self::log_message( $message, $context, $use_elementor_logger, 'info' );
}
public static function warning( string $message, array $context = [], bool $use_elementor_logger = false ): void {
self::log_message( $message, $context, $use_elementor_logger, 'warning' );
}
public static function error( string $message, array $context = [], bool $use_elementor_logger = false ): void {
self::log_message( $message, $context, $use_elementor_logger, 'error' );
}
private static function log_message( string $message, array $context, bool $use_elementor_logger, string $level ): void {
if ( $use_elementor_logger ) {
self::log_to_elementor_db( $message, $context, $level );
return;
}
self::log_to_wp_debug_file( $message, $context, $level );
}
private static function log_to_wp_debug_file( string $message, array $context, string $level ): void {
if ( ! self::should_log_to_file() ) {
return;
}
$formatted_message = self::format_message( $message, $context, $level );
error_log( $formatted_message );
}
private static function log_to_elementor_db( string $message, array $context, string $level ): void {
if ( ! isset( \Elementor\Plugin::$instance->logger ) ) {
return;
}
try {
$logger = \Elementor\Plugin::$instance->logger;
switch ( $level ) {
case 'error':
$logger->error( $message, $context );
break;
case 'warning':
$logger->warning( $message, $context );
break;
default:
$logger->info( $message, $context );
break;
}
} catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Logger must not throw exceptions
}
}
private static function should_log_to_file(): bool {
$debug_log_enabled = defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG;
$debug_display_disabled = ! defined( 'WP_DEBUG_DISPLAY' ) || ! WP_DEBUG_DISPLAY;
return $debug_log_enabled && $debug_display_disabled;
}
private static function format_message( string $message, array $context, string $level ): string {
$level_prefix = strtoupper( $level );
$formatted = "[Elementor Atomic Widgets] [{$level_prefix}] " . $message;
if ( ! empty( $context ) ) {
$context_json = wp_json_encode( $context, JSON_UNESCAPED_SLASHES );
if ( false !== $context_json ) {
$formatted .= ' | Context: ' . $context_json;
}
}
return $formatted;
}
}

View File

@@ -0,0 +1,423 @@
<?php
namespace Elementor\Modules\AtomicWidgets;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Experiments\Manager as Experiments_Manager;
use Elementor\Elements_Manager;
use Elementor\Modules\AtomicWidgets\DynamicTags\Dynamic_Tags_Module;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Youtube\Atomic_Youtube;
use Elementor\Modules\AtomicWidgets\Elements\Div_Block\Div_Block;
use Elementor\Modules\AtomicWidgets\Elements\Flexbox\Flexbox;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Heading\Atomic_Heading;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Image\Atomic_Image;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Paragraph\Atomic_Paragraph;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Button\Atomic_Button;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Divider\Atomic_Divider;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Svg\Atomic_Svg;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Tabs\Atomic_Tabs;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Tabs\Atomic_Tabs_Menu;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Tabs\Atomic_Tab;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Tabs\Atomic_Tabs_Content_Area;
use Elementor\Modules\AtomicWidgets\ImportExport\Atomic_Import_Export;
use Elementor\Modules\AtomicWidgets\Elements\Loader\Frontend_Assets_Loader;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Combine_Array_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Export\Image_Src_Export_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Image_Src_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Image_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Import\Image_Src_Import_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Import_Export_Plain_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Settings\Classes_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Settings\Date_Time_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Settings\Link_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Plain_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Background_Color_Overlay_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Background_Gradient_Overlay_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Background_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Color_Stop_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Multi_Props_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Position_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Shadow_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Size_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Stroke_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Background_Image_Overlay_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Background_Image_Overlay_Size_Scale_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Background_Overlay_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Filter_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Transform_Origin_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Transition_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Transform_Rotate_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Transform_Skew_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Transform_Functions_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Transform_Move_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Flex_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Transform_Scale_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Settings\Attributes_Transformer;
use Elementor\Modules\AtomicWidgets\PropTypes\Attributes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers_Registry;
use Elementor\Modules\AtomicWidgets\PropTypes\Background_Color_Overlay_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Background_Gradient_Overlay_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Background_Image_Overlay_Size_Scale_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Background_Image_Overlay_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Background_Image_Position_Offset_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Background_Overlay_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Background_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Box_Shadow_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Border_Radius_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Border_Width_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Color_Stop_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Date_Time_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Filters\Backdrop_Filter_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Filters\Filter_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Gradient_Color_Stop_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Layout_Direction_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Flex_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Link_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Classes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Image_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Image_Src_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Dimensions_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Position_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Shadow_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Size_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Stroke_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Transform\Functions\Transform_Move_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Transform\Transform_Functions_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Transform\Transform_Origin_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Transform\Functions\Transform_Scale_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Transform\Transform_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Transform\Functions\Transform_Rotate_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Transform\Functions\Transform_Skew_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Transition_Prop_Type;
use Elementor\Modules\AtomicWidgets\Styles\Atomic_Styles_Manager;
use Elementor\Modules\AtomicWidgets\Styles\Atomic_Widget_Base_Styles;
use Elementor\Modules\AtomicWidgets\Styles\Atomic_Widget_Styles;
use Elementor\Modules\AtomicWidgets\Styles\Size_Constants;
use Elementor\Modules\AtomicWidgets\Styles\Style_Schema;
use Elementor\Modules\AtomicWidgets\Database\Atomic_Widgets_Database_Updater;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Tabs\Atomic_Tab_Content;
use Elementor\Modules\AtomicWidgets\PropTypeMigrations\Migrations_Orchestrator;
use Elementor\Plugin;
use Elementor\Widgets_Manager;
use Elementor\Modules\AtomicWidgets\Library\Atomic_Widgets_Library;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Settings\Query_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Perspective_Origin_Transformer;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\Number_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Query_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Transform\Perspective_Origin_Prop_Type;
use Elementor\Modules\AtomicWidgets\Utils\Utils;
use Elementor\Core\Base\Document;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseModule {
const EXPERIMENT_NAME = 'e_atomic_elements';
const ENFORCE_CAPABILITIES_EXPERIMENT = 'atomic_widgets_should_enforce_capabilities';
const EXPERIMENT_EDITOR_MCP = 'editor_mcp';
const EXPERIMENT_BC_MIGRATIONS = 'e_bc_migrations';
const PACKAGES = [
'editor-canvas',
'editor-controls', // TODO: Need to be registered and not enqueued.
'editor-editing-panel',
'editor-elements', // TODO: Need to be registered and not enqueued.
'editor-props', // TODO: Need to be registered and not enqueued.
'editor-styles', // TODO: Need to be registered and not enqueued.
'editor-styles-repository',
'editor-interactions',
'editor-templates',
];
public function get_name() {
return 'atomic-widgets';
}
public function __construct() {
parent::__construct();
if ( self::is_active() ) {
$this->register_experimental_features();
Migrations_Orchestrator::register_feature_flag_hooks();
}
if ( Plugin::$instance->experiments->is_feature_active( self::EXPERIMENT_NAME ) ) {
Dynamic_Tags_Module::instance()->register_hooks();
( new Atomic_Widget_Styles() )->register_hooks();
( new Atomic_Widget_Base_Styles() )->register_hooks();
( new Atomic_Widgets_Library() )->register_hooks();
Atomic_Styles_Manager::instance()->register_hooks();
( new Atomic_Import_Export() )->register_hooks();
( new Atomic_Widgets_Database_Updater() )->register();
add_filter( 'elementor/editor/v2/packages', fn ( $packages ) => $this->add_packages( $packages ) );
add_filter( 'elementor/editor/localize_settings', fn ( $settings ) => $this->add_styles_schema( $settings ) );
add_filter( 'elementor/editor/localize_settings', fn ( $settings ) => $this->add_supported_units( $settings ) );
add_filter( 'elementor/widgets/register', fn ( Widgets_Manager $widgets_manager ) => $this->register_widgets( $widgets_manager ) );
add_filter( 'elementor/usage/elements/element_title', fn ( $title, $type ) => $this->get_element_usage_name( $title, $type ), 10, 2 );
add_filter( 'elementor/document/load/data', fn ( $data, $document ) => $this->backward_compatibility_migrations( $data, $document ), 10, 2 );
add_action( 'elementor/elements/elements_registered', fn ( $elements_manager ) => $this->register_elements( $elements_manager ) );
add_action( 'elementor/editor/after_enqueue_scripts', fn () => $this->enqueue_scripts() );
add_action( 'elementor/frontend/before_register_scripts', fn () => $this->register_frontend_scripts() );
add_action( 'elementor/frontend/after_enqueue_styles', fn () => $this->add_inline_styles() );
add_action( 'elementor/atomic-widgets/settings/transformers/register', fn ( $transformers ) => $this->register_settings_transformers( $transformers ) );
add_action( 'elementor/atomic-widgets/styles/transformers/register', fn ( $transformers ) => $this->register_styles_transformers( $transformers ) );
add_action( 'elementor/atomic-widgets/import/transformers/register', fn ( $transformers ) => $this->register_import_transformers( $transformers ) );
add_action( 'elementor/atomic-widgets/export/transformers/register', fn ( $transformers ) => $this->register_export_transformers( $transformers ) );
add_action( 'elementor/editor/templates/panel/category', fn () => $this->render_panel_category_chip() );
}
}
public static function get_experimental_data() {
return [
'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 register_experimental_features() {
Plugin::$instance->experiments->add_feature( [
'name' => 'e_indications_popover',
'title' => esc_html__( 'V4 Indications Popover', 'elementor' ),
'description' => esc_html__( 'Enable V4 Indication Popovers', 'elementor' ),
'hidden' => true,
'default' => Experiments_Manager::STATE_INACTIVE,
] );
Plugin::$instance->experiments->add_feature( [
'name' => self::ENFORCE_CAPABILITIES_EXPERIMENT,
'title' => esc_html__( 'Enforce atomic widgets capabilities', 'elementor' ),
'description' => esc_html__( 'Enforce atomic widgets capabilities.', 'elementor' ),
'hidden' => true,
'default' => Experiments_Manager::STATE_ACTIVE,
'release_status' => Experiments_Manager::RELEASE_STATUS_DEV,
] );
Plugin::$instance->experiments->add_feature([
'name' => self::EXPERIMENT_EDITOR_MCP,
'title' => esc_html__( 'Editor MCP for atomic widgets', 'elementor' ),
'description' => esc_html__( 'Editor MCP for atomic widgets.', 'elementor' ),
'hidden' => true,
'default' => Experiments_Manager::STATE_ACTIVE,
'release_status' => Experiments_Manager::RELEASE_STATUS_DEV,
]);
Plugin::$instance->experiments->add_feature([
'name' => self::EXPERIMENT_BC_MIGRATIONS,
'title' => esc_html__( 'Backward compatibility migrations', 'elementor' ),
'description' => esc_html__( 'Enable automatic prop type migrations for atomic widgets', 'elementor' ),
'hidden' => true,
'default' => Experiments_Manager::STATE_ACTIVE,
'release_status' => Experiments_Manager::RELEASE_STATUS_DEV,
]);
}
private function add_packages( $packages ) {
return array_merge( $packages, self::PACKAGES );
}
private function add_styles_schema( $settings ) {
if ( ! isset( $settings['atomic'] ) ) {
$settings['atomic'] = [];
}
$settings['atomic']['styles_schema'] = Style_Schema::get();
return $settings;
}
private function add_supported_units( $settings ) {
$settings['supported_size_units'] = Size_Constants::all_supported_units();
return $settings;
}
private function register_widgets( Widgets_Manager $widgets_manager ) {
$widgets_manager->register( new Atomic_Heading() );
$widgets_manager->register( new Atomic_Image() );
$widgets_manager->register( new Atomic_Paragraph() );
$widgets_manager->register( new Atomic_Svg() );
$widgets_manager->register( new Atomic_Button() );
$widgets_manager->register( new Atomic_Youtube() );
$widgets_manager->register( new Atomic_Divider() );
}
private function register_elements( Elements_Manager $elements_manager ) {
$elements_manager->register_element_type( new Div_Block() );
$elements_manager->register_element_type( new Flexbox() );
$elements_manager->register_element_type( new Atomic_Tabs() );
$elements_manager->register_element_type( new Atomic_Tabs_Menu() );
$elements_manager->register_element_type( new Atomic_Tab() );
$elements_manager->register_element_type( new Atomic_Tabs_Content_Area() );
$elements_manager->register_element_type( new Atomic_Tab_Content() );
}
private function register_settings_transformers( Transformers_Registry $transformers ) {
$transformers->register_fallback( new Plain_Transformer() );
$transformers->register( Classes_Prop_Type::get_key(), new Classes_Transformer() );
$transformers->register( Image_Prop_Type::get_key(), new Image_Transformer() );
$transformers->register( Image_Src_Prop_Type::get_key(), new Image_Src_Transformer() );
$transformers->register( Link_Prop_Type::get_key(), new Link_Transformer() );
$transformers->register( Query_Prop_Type::get_key(), new Query_Transformer() );
$transformers->register( Attributes_Prop_Type::get_key(), new Attributes_Transformer() );
$transformers->register( Date_Time_Prop_Type::get_key(), new Date_Time_Transformer() );
}
private function register_styles_transformers( Transformers_Registry $transformers ) {
$transformers->register_fallback( new Plain_Transformer() );
$transformers->register( Size_Prop_Type::get_key(), new Size_Transformer() );
$transformers->register( Box_Shadow_Prop_Type::get_key(), new Combine_Array_Transformer( ',' ) );
$transformers->register( Shadow_Prop_Type::get_key(), new Shadow_Transformer() );
$transformers->register( Flex_Prop_Type::get_key(), new Flex_Transformer() );
$transformers->register( Stroke_Prop_Type::get_key(), new Stroke_Transformer() );
$transformers->register( Image_Prop_Type::get_key(), new Image_Transformer() );
$transformers->register( Image_Src_Prop_Type::get_key(), new Image_Src_Transformer() );
$transformers->register( Background_Image_Overlay_Prop_Type::get_key(), new Background_Image_Overlay_Transformer() );
$transformers->register( Background_Image_Overlay_Size_Scale_Prop_Type::get_key(), new Background_Image_Overlay_Size_Scale_Transformer() );
$transformers->register( Background_Image_Position_Offset_Prop_Type::get_key(), new Position_Transformer() );
$transformers->register( Background_Color_Overlay_Prop_Type::get_key(), new Background_Color_Overlay_Transformer() );
$transformers->register( Background_Overlay_Prop_Type::get_key(), new Background_Overlay_Transformer() );
$transformers->register( Background_Prop_Type::get_key(), new Background_Transformer() );
$transformers->register( Background_Gradient_Overlay_Prop_Type::get_key(), new Background_Gradient_Overlay_Transformer() );
$transformers->register( Filter_Prop_Type::get_key(), new Filter_Transformer() );
$transformers->register( Backdrop_Filter_Prop_Type::get_key(), new Filter_Transformer() );
$transformers->register( Transition_Prop_Type::get_key(), new Transition_Transformer() );
$transformers->register( Color_Stop_Prop_Type::get_key(), new Color_Stop_Transformer() );
$transformers->register( Gradient_Color_Stop_Prop_Type::get_key(), new Combine_Array_Transformer( ',' ) );
$transformers->register( Position_Prop_Type::get_key(), new Position_Transformer() );
$transformers->register( Transform_Move_Prop_Type::get_key(), new Transform_Move_Transformer() );
$transformers->register( Transform_Scale_Prop_Type::get_key(), new Transform_Scale_Transformer() );
$transformers->register( Transform_Rotate_Prop_Type::get_key(), new Transform_Rotate_Transformer() );
$transformers->register( Transform_Skew_Prop_Type::get_key(), new Transform_Skew_Transformer() );
$transformers->register( Transform_Functions_Prop_Type::get_key(), new Transform_Functions_Transformer() );
$transformers->register( Transform_Origin_Prop_Type::get_key(), new Transform_Origin_Transformer() );
$transformers->register( Perspective_Origin_Prop_Type::get_key(), new Perspective_Origin_Transformer() );
$transformers->register(
Transform_Prop_Type::get_key(),
new Multi_Props_Transformer(
[ 'transform-functions', 'transform-origin', 'perspective', 'perspective-origin' ],
fn( $_, $key ) => 'transform-functions' === $key ? 'transform' : $key
)
);
$transformers->register(
Border_Radius_Prop_Type::get_key(),
new Multi_Props_Transformer( [ 'start-start', 'start-end', 'end-start', 'end-end' ], fn ( $_, $key ) => "border-{$key}-radius" )
);
$transformers->register(
Border_Width_Prop_Type::get_key(),
new Multi_Props_Transformer( [ 'block-start', 'block-end', 'inline-start', 'inline-end' ], fn ( $_, $key ) => "border-{$key}-width" )
);
$transformers->register(
Layout_Direction_Prop_Type::get_key(),
new Multi_Props_Transformer( [ 'column', 'row' ], fn ( $prop_key, $key ) => "{$key}-{$prop_key}" )
);
$transformers->register(
Dimensions_Prop_Type::get_key(),
new Multi_Props_Transformer( [ 'block-start', 'block-end', 'inline-start', 'inline-end' ], fn ( $prop_key, $key ) => "{$prop_key}-{$key}" )
);
}
public function register_import_transformers( Transformers_Registry $transformers ) {
$transformers->register_fallback( new Import_Export_Plain_Transformer() );
$transformers->register( Image_Src_Prop_Type::get_key(), new Image_Src_Import_Transformer() );
}
public function register_export_transformers( Transformers_Registry $transformers ) {
$transformers->register_fallback( new Import_Export_Plain_Transformer() );
$transformers->register( Image_Src_Prop_Type::get_key(), new Image_Src_Export_Transformer() );
}
public static function is_active(): bool {
return Plugin::$instance->experiments->is_feature_active( self::EXPERIMENT_NAME );
}
private function get_element_usage_name( $title, $type ) {
$element_instance = Plugin::$instance->elements_manager->get_element_types( $type );
$widget_instance = Plugin::$instance->widgets_manager->get_widget_types( $type );
if ( Utils::is_atomic( $element_instance ) || Utils::is_atomic( $widget_instance ) ) {
return $type;
}
return $title;
}
/**
* 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
);
}
private function render_panel_category_chip() {
?><# if ( 'v4-elements' === name ) { #>
<span class="elementor-panel-heading-category-chip">
<?php echo esc_html__( 'Beta', 'elementor' ); ?><i class="eicon-info"></i>
<span class="e-promotion-react-wrapper" data-promotion="v4_chip"></span>
</span>
<# } #><?php
}
private function register_frontend_scripts() {
$loader = new Frontend_Assets_Loader();
$loader->register_scripts();
}
private function add_inline_styles() {
$inline_css = '.e-heading-base a, .e-paragraph-base a { all: unset; cursor: pointer; }';
wp_add_inline_style( 'elementor-frontend', $inline_css );
}
private function backward_compatibility_migrations( array $data, $document ): array {
if ( ! Plugin::$instance->experiments->is_feature_active( self::EXPERIMENT_BC_MIGRATIONS ) ) {
return $data;
}
$orchestrator = Migrations_Orchestrator::make( $this->get_migrations_base_path() );
$orchestrator->migrate_document(
$data,
$document->get_post()->ID,
function( $migrated_data ) use ( $document ) {
$document->update_json_meta(
Document::ELEMENTOR_DATA_META_KEY,
$migrated_data
);
}
);
return $data;
}
private function get_migrations_base_path(): string {
// define this in wp-config.php to use local migrations i.e. __DIR__ . '/wp-content/plugins/elementor/migrations/'
if ( defined( 'ELEMENTOR_MIGRATIONS_PATH' ) ) {
return ELEMENTOR_MIGRATIONS_PATH;
}
return 'https://migrations.elementor.com/';
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Elementor\Modules\AtomicWidgets\OptIn;
use Elementor\Core\Common\Modules\Ajax\Module as Ajax;
use Elementor\Core\Experiments\Manager as Experiments_Manager;
use Elementor\Modules\GlobalClasses\Module as GlobalClassesModule;
use Elementor\Modules\NestedElements\Module as NestedElementsModule;
use Elementor\Modules\AtomicWidgets\Module as AtomicWidgetsModule;
use Elementor\Modules\Variables\Module as VariablesModule;
use Elementor\Modules\Components\Module as ComponentsModule;
use Elementor\Plugin;
class Opt_In {
const EXPERIMENT_NAME = 'e_opt_in_v4';
const OPT_OUT_FEATURES = [
self::EXPERIMENT_NAME,
AtomicWidgetsModule::EXPERIMENT_NAME,
GlobalClassesModule::NAME,
VariablesModule::EXPERIMENT_NAME,
ComponentsModule::EXPERIMENT_NAME,
];
const OPT_IN_FEATURES = [
self::EXPERIMENT_NAME,
'container',
NestedElementsModule::EXPERIMENT_NAME,
AtomicWidgetsModule::EXPERIMENT_NAME,
GlobalClassesModule::NAME,
VariablesModule::EXPERIMENT_NAME,
ComponentsModule::EXPERIMENT_NAME,
];
public function init() {
$this->register_feature();
add_action( 'elementor/ajax/register_actions', fn( Ajax $ajax ) => $this->add_ajax_actions( $ajax ) );
}
private function register_feature() {
Plugin::$instance->experiments->add_feature([
'name' => self::EXPERIMENT_NAME,
'title' => esc_html__( 'Editor V4', 'elementor' ),
'description' => esc_html__( 'Enable Editor V4.', 'elementor' ),
'hidden' => true,
'default' => Experiments_Manager::STATE_INACTIVE,
'release_status' => Experiments_Manager::RELEASE_STATUS_ALPHA,
]);
}
private function opt_out_v4() {
foreach ( self::OPT_OUT_FEATURES as $feature ) {
$feature_key = Plugin::$instance->experiments->get_feature_option_key( $feature );
update_option( $feature_key, Experiments_Manager::STATE_INACTIVE );
}
}
private function opt_in_v4() {
foreach ( self::OPT_IN_FEATURES as $feature ) {
$feature_key = Plugin::$instance->experiments->get_feature_option_key( $feature );
update_option( $feature_key, Experiments_Manager::STATE_ACTIVE );
}
}
public function ajax_opt_out_v4() {
if ( ! current_user_can( 'manage_options' ) ) {
throw new \Exception( 'Permission denied' );
}
$this->opt_out_v4();
}
public function ajax_opt_in_v4() {
if ( ! current_user_can( 'manage_options' ) ) {
throw new \Exception( 'Permission denied' );
}
$this->opt_in_v4();
}
private function add_ajax_actions( Ajax $ajax ) {
$ajax->register_ajax_action( 'editor_v4_opt_in', fn() => $this->ajax_opt_in_v4() );
$ajax->register_ajax_action( 'editor_v4_opt_out', fn() => $this->ajax_opt_out_v4() );
}
}

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