first commit

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

View File

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

View File

@@ -0,0 +1,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\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__( 'Editor V4', '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,64 @@
<?php
namespace Elementor\Modules\AtomicWidgets\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\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,24 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Style_Transformer_Base {
/**
* Get the transformer type.
*
* @return string
*/
abstract public static function type(): string;
/**
* Transform the value.
*
* @param mixed $value
*
* @return mixed
*/
abstract public function transform( $value, callable $transform );
}

View File

@@ -0,0 +1,120 @@
<?php
namespace Elementor\Modules\AtomicWidgets;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Cache_Validity {
const CACHE_KEY_PREFIX = 'elementor_atomic_cache_validity-';
public function is_valid( array $keys ): bool {
$root = array_shift( $keys );
$state_item = get_option( self::CACHE_KEY_PREFIX . $root, null );
if ( ! empty( $keys ) ) {
if ( ! $state_item ) {
return false;
}
$state_item = $this->get_nested_item( $state_item, $keys );
}
return $state_item ? $state_item['state'] : false;
}
public function get_meta( array $keys ) {
$root = array_shift( $keys );
$state_item = get_option( self::CACHE_KEY_PREFIX . $root, null );
if ( ! $state_item ) {
return null;
}
$state_item = $this->get_nested_item( $state_item, $keys );
return isset( $state_item['meta'] ) ? $state_item['meta'] : null;
}
public function invalidate( array $keys ): void {
$root = array_shift( $keys );
$state_item = get_option( self::CACHE_KEY_PREFIX . $root, [
'state' => false,
'meta' => null,
'children' => [],
] );
$current_item = &$state_item;
if ( ! empty( $keys ) ) {
$current_item = &$this->get_nested_item( $current_item, $keys );
}
$current_item['state'] = false;
$current_item['meta'] = null;
$this->invalidate_nested_items( $current_item );
update_option( self::CACHE_KEY_PREFIX . $root, $state_item );
}
public function validate( array $keys, $meta = null ): void {
$root = array_shift( $keys );
$state_item = get_option( self::CACHE_KEY_PREFIX . $root, [
'state' => false,
'children' => [],
] );
$current_item = &$state_item;
if ( ! empty( $keys ) ) {
$current_item = &$this->get_nested_item( $current_item, $keys );
}
$current_item['state'] = true;
$current_item['meta'] = $meta;
update_option( self::CACHE_KEY_PREFIX . $root, $state_item );
}
/**
* @param array{state: boolean, meta: array<string, mixed> | null, children: array<string, self>} $root_item
* @param array<string> $keys
* @return array{state: boolean, meta: array<string, mixed> | null, children: array<string, self>}
*/
private function &get_nested_item( array &$root_item, array $keys ): array {
$current_item = &$root_item;
while ( ! empty( $keys ) ) {
$key = array_shift( $keys );
if ( ! isset( $current_item['children'][ $key ] ) ) {
$current_item['children'][ $key ] = [
'state' => false,
'meta' => null,
'children' => [],
];
}
$current_item = &$current_item['children'][ $key ];
}
return $current_item;
}
private function invalidate_nested_items( array &$root_item ): void {
foreach ( $root_item['children'] as &$child_item ) {
$child_item['state'] = false;
$child_item['meta'] = null;
$this->invalidate_nested_items( $child_item );
}
}
}

View File

@@ -0,0 +1,68 @@
<?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 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\Elements;
use Elementor\Modules\AtomicWidgets\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\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,30 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls\Types;
use Elementor\Modules\AtomicWidgets\Base\Atomic_Control_Base;
use Elementor\Modules\AtomicWidgets\Image\Image_Sizes;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Image_Control extends Atomic_Control_Base {
private string $show_mode = 'all';
public function set_show_mode( string $show_mode ): self {
$this->show_mode = $show_mode;
return $this;
}
public function get_type(): string {
return 'image';
}
public function get_props(): array {
return [
'sizes' => Image_Sizes::get_all(),
'showMode' => $this->show_mode,
];
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls\Types;
use Elementor\Modules\AtomicWidgets\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\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\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\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\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\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\Base\Atomic_Control_Base;
use Elementor\Modules\AtomicWidgets\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\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\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\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\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,82 @@
<?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::add_meta()`, e.g. `String_Prop_Type::make()->add_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'] ?? [];
}
protected function validate_value( $value ): bool {
$is_valid_structure = (
isset( $value['name'] ) &&
is_string( $value['name'] ) &&
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'],
'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,118 @@
<?php
namespace Elementor\Modules\AtomicWidgets\DynamicTags;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Array_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Object_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Transformable_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\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\Union_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Url_Prop_Type;
use Elementor\Modules\DynamicTags\Module as V1_Dynamic_Tags_Module;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Dynamic_Prop_Types_Mapping {
public static function make(): self {
return new static();
}
/**
* @param array<string, Prop_Type> $schema
*
* @return array<string, Prop_Type>
*/
public function get_modified_prop_types( array $schema ): array {
$result = [];
foreach ( $schema as $key => $prop_type ) {
if ( ! ( $prop_type instanceof Prop_Type ) ) {
$result[ $key ] = $prop_type;
continue;
}
$result[ $key ] = $this->get_modified_prop_type( $prop_type );
}
return $result;
}
/**
* Change prop type into a union prop type if the original prop type supports dynamic tags.
*
* @param Prop_Type $prop_type
*
* @return Prop_Type|Union_Prop_Type
*/
private function get_modified_prop_type( Prop_Type $prop_type ) {
$transformable_prop_types = $prop_type instanceof Union_Prop_Type ?
$prop_type->get_prop_types() :
[ $prop_type ];
$categories = [];
foreach ( $transformable_prop_types as $transformable_prop_type ) {
if ( $transformable_prop_type instanceof Object_Prop_Type ) {
$transformable_prop_type->set_shape(
$this->get_modified_prop_types( $transformable_prop_type->get_shape() )
);
}
if ( $transformable_prop_type instanceof Array_Prop_Type ) {
$transformable_prop_type->set_item_type(
$this->get_modified_prop_type( $transformable_prop_type->get_item_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 $prop_type;
}
$dynamic_prop_type = Dynamic_Prop_Type::make()->categories( $categories );
$union_prop_type = $prop_type;
if ( $prop_type instanceof Transformable_Prop_Type ) {
$union_prop_type = Union_Prop_Type::create_from( $prop_type );
}
$union_prop_type->add_prop_type( $dynamic_prop_type );
return $union_prop_type;
}
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 instanceof Image_Src_Prop_Type ) {
return [ V1_Dynamic_Tags_Module::IMAGE_CATEGORY ];
}
if ( $prop_type instanceof String_Prop_Type && empty( $prop_type->get_enum() ) ) {
return [ V1_Dynamic_Tags_Module::TEXT_CATEGORY ];
}
if ( $prop_type instanceof Url_Prop_Type ) {
return [ V1_Dynamic_Tags_Module::URL_CATEGORY ];
}
return [];
}
}

View File

@@ -0,0 +1,131 @@
<?php
namespace Elementor\Modules\AtomicWidgets\DynamicTags;
use Elementor\Modules\AtomicWidgets\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\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 = String_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,384 @@
<?php
namespace Elementor\Modules\AtomicWidgets\DynamicTags;
use Elementor\Modules\AtomicWidgets\Controls\Section;
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['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_text_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'] );
}
/**
* @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_show_mode( 'media' )
->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,78 @@
<?php
namespace Elementor\Modules\AtomicWidgets\DynamicTags;
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_modified_prop_types( $schema )
);
add_action(
'elementor/atomic-widgets/settings/transformers/register',
fn ( $transformers, $prop_resolver ) => $this->register_transformers( $transformers, $prop_resolver ),
10,
2
);
}
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
)
);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Elementor\Modules\AtomicWidgets\DynamicTags;
use Elementor\Core\DynamicTags\Base_Tag;
use Elementor\Modules\AtomicWidgets\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,18 @@
{% 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 %}
<a
href="{{ settings.link.href | raw }}"
target="{{ settings.link.target }}"
class="{{ classes }}"
{{ id_attribute }} {{ settings.attributes | raw }}
>
{{ settings.text }}
</a>
{% else %}
<button class="{{ classes }}" {{ id_attribute }} {{ settings.attributes | raw }}>
{{ settings.text }}
</button>
{% endif %}
{% endif %}

View File

@@ -0,0 +1,139 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Atomic_Button;
use Elementor\Modules\AtomicWidgets\Controls\Types\Text_Control;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Widget_Base;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Modules\AtomicWidgets\Controls\Types\Link_Control;
use Elementor\Modules\AtomicWidgets\Elements\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;
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' ) ),
'link' => Link_Prop_Type::make(),
'attributes' => Attributes_Prop_Type::make(),
];
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 }}" {{ id_attribute }} {{ settings.attributes | raw }} />

View File

@@ -0,0 +1,107 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Atomic_Divider;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Widget_Base;
use Elementor\Modules\AtomicWidgets\Elements\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;
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(),
];
}
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,181 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements;
use Elementor\Element_Base;
use Elementor\Modules\AtomicWidgets\PropDependencies\Manager as Dependency_Manager;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Prop_Type;
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;
protected $version = '0.0';
protected $styles = [];
protected $editor_settings = [];
public function __construct( $data = [], $args = null ) {
parent::__construct( $data, $args );
$this->version = $data['version'] ?? '0.0';
$this->styles = $data['styles'] ?? [];
$this->editor_settings = $data['editor_settings'] ?? [];
}
abstract protected function define_atomic_controls(): array;
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['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();
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 [];
}
/**
* 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'] ) ? 'a' : ( $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'] );
}
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,12 @@
{% 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') }} class="{{ settings.classes | merge( [ base_styles.base ] ) | join(' ') }}" {{ id_attribute }} {{ settings.attributes | raw }}>
{% if settings.link.href %}
<a href="{{ settings.link.href }}" target="{{ settings.link.target }}" class="{{ base_styles['link-base'] }}">
{{ settings.title }}
</a>
{% else %}
{{ settings.title }}
{% endif %}
</{{ settings.tag | e('html_tag') }}>
{% endif %}

View File

@@ -0,0 +1,150 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Atomic_Heading;
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\Controls\Types\Textarea_Control;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Widget_Base;
use Elementor\Modules\AtomicWidgets\Elements\Has_Template;
use Elementor\Modules\AtomicWidgets\PropTypes\Classes_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;
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 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 {
$props = [
'classes' => Classes_Prop_Type::make()
->default( [] ),
'tag' => String_Prop_Type::make()
->enum( [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ] )
->default( 'h2' ),
'title' => String_Prop_Type::make()
->default( __( 'This is a title', 'elementor' ) ),
'link' => Link_Prop_Type::make(),
'attributes' => Attributes_Prop_Type::make(),
];
return $props;
}
protected function define_atomic_controls(): array {
$content_section = Section::make()
->set_label( __( 'Content', 'elementor' ) )
->set_items( [
Textarea_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,18 @@
{% 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 %}
<a href="{{ settings.link.href }}" class="{{ base_styles['link-base'] }}" target="{{ settings.link.target }}">
{% endif %}
<img class="{{ base_styles['base'] }} {{ settings.classes | join(' ') }}" {{ 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 %}
</a>
{% endif %}
{% endif %}

View File

@@ -0,0 +1,116 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Atomic_Image;
use Elementor\Modules\AtomicWidgets\Controls\Types\Link_Control;
use Elementor\Modules\AtomicWidgets\Elements\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\Atomic_Widget_Base;
use Elementor\Modules\AtomicWidgets\PropTypes\Attributes_Prop_Type;
use Elementor\Modules\AtomicWidgets\Controls\Types\Image_Control;
use Elementor\Modules\AtomicWidgets\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;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Atomic_Image extends Atomic_Widget_Base {
use Has_Template;
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(),
];
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_show_mode( 'media' )
->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 [
Image_Control::bind_to( 'image' )
->set_show_mode( 'sizes' )
->set_label( __( 'Image resolution', 'elementor' ) )
->set_meta( [ 'layout' => 'two-columns' ] ),
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 {
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,12 @@
{% if settings.paragraph is not empty %}
{% set id_attribute = settings._cssid is not empty ? 'id=' ~ settings._cssid | e('html_attr') : '' %}
<p class="{{ settings.classes | merge( [ base_styles.base ] ) | join(' ') }}" {{ id_attribute }} {{ settings.attributes | raw }}>
{% if settings.link.href %}
<a href="{{ settings.link.href }}" target="{{ settings.link.target }}" class="{{ base_styles['link-base'] }}">
{{ settings.paragraph }}
</a>
{% else %}
{{ settings.paragraph }}
{% endif %}
</p>
{% endif %}

View File

@@ -0,0 +1,113 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Atomic_Paragraph;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Widget_Base;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Modules\AtomicWidgets\Controls\Types\Link_Control;
use Elementor\Modules\AtomicWidgets\Controls\Types\Textarea_Control;
use Elementor\Modules\AtomicWidgets\Elements\Has_Template;
use Elementor\Modules\AtomicWidgets\PropTypes\Classes_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;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Atomic_Paragraph extends Atomic_Widget_Base {
use Has_Template;
const LINK_BASE_STYLE_KEY = 'link-base';
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 {
$props = [
'classes' => Classes_Prop_Type::make()
->default( [] ),
'paragraph' => String_Prop_Type::make()
->default( __( 'Type your paragraph here', 'elementor' ) ),
'link' => Link_Prop_Type::make(),
'attributes' => Attributes_Prop_Type::make(),
];
return $props;
}
protected function define_atomic_controls(): array {
return [
Section::make()
->set_label( __( 'Content', 'elementor' ) )
->set_items( [
Textarea_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 [
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 {
$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,191 @@
<?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\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\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 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 ),
'link' => Link_Prop_Type::make(),
'attributes' => Attributes_Prop_Type::make(),
];
}
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' );
$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'] );
if ( isset( $settings['link'] ) && ! empty( $settings['link']['href'] ) ) {
$svg_html = sprintf(
'<a href="%s" target="%s" class="%s" %s>%s</a>',
$settings['link']['href'],
esc_attr( $settings['link']['target'] ),
esc_attr( $classes_string ),
$all_attributes,
$svg_html
);
} else {
$svg_html = sprintf( '<div class="%s" %s>%s</div>', esc_attr( $classes_string ), $all_attributes, $svg_html );
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $svg_html;
}
private function get_svg_content( $settings ) {
if ( isset( $settings['svg']['id'] ) ) {
$content = Utils::file_get_contents(
get_attached_file( $settings['svg']['id'] )
);
if ( $content ) {
return $content;
}
}
if (
isset( $settings['svg']['url'] ) &&
static::DEFAULT_SVG_URL !== $settings['svg']['url']
) {
$content = wp_safe_remote_get(
$settings['svg']['url']
);
if ( ! is_wp_error( $content ) ) {
return $content['body'];
}
}
$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,135 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Atomic_Tabs;
use Elementor\Modules\AtomicWidgets\Elements\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\Elements\Atomic_Paragraph\Atomic_Paragraph;
use Elementor\Modules\AtomicWidgets\PropTypes\Classes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Attributes_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Atomic_Tab_Panel extends Atomic_Element_Base {
const BASE_STYLE_KEY = 'base';
public static function get_type() {
return 'e-tab-panel';
}
public static function get_element_type(): string {
return 'e-tab-panel';
}
public function get_title() {
return esc_html__( 'Atomic Tab Panel', 'elementor' );
}
public function get_keywords() {
return [ 'ato', 'atom', 'atoms', 'atomic', 'tab', 'panel', 'tabs' ];
}
public function get_icon() {
return 'eicon-tabs';
}
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(),
];
}
protected function define_atomic_controls(): array {
return [
Section::make()
->set_label( __( 'Settings', 'elementor' ) )
->set_id( 'settings' )
->set_items( [] ),
];
}
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 define_initial_attributes() {
return [
'role' => 'tabpanel',
];
}
protected function define_default_children() {
return [
Atomic_Paragraph::generate()
->settings( [
'text' => String_Prop_Type::generate( 'Tab Content' ),
] )
->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();
$attributes = [
'class' => [
'e-con',
'e-atomic-element',
$base_style_class,
...( $settings['classes'] ?? [] ),
],
];
if ( ! empty( $settings['tab-id'] ) ) {
$attributes['data-tab-id'] = esc_attr( $settings['tab-id'] );
$attributes['aria-labelledby'] = esc_attr( $settings['tab-id'] );
}
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,127 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Atomic_Tabs;
use Elementor\Modules\AtomicWidgets\Elements\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\Controls\Section;
use Elementor\Modules\AtomicWidgets\Controls\Types\Text_Control;
use Elementor\Modules\AtomicWidgets\PropTypes\Classes_Prop_Type;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Heading\Atomic_Heading;
use Elementor\Modules\AtomicWidgets\PropTypes\Attributes_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Atomic_Tab extends Atomic_Element_Base {
const BASE_STYLE_KEY = 'base';
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__( 'Atomic Tab', 'elementor' );
}
public function get_keywords() {
return [ 'ato', 'atom', 'atoms', 'atomic' ];
}
public function get_icon() {
return 'eicon-tabs';
}
public function should_show_in_panel() {
return false;
}
protected static function define_props_schema(): array {
return [
'classes' => Classes_Prop_Type::make()
->default( [] ),
'tab-panel-id' => String_Prop_Type::make(),
'attributes' => Attributes_Prop_Type::make(),
];
}
protected function define_atomic_controls(): array {
return [
Section::make()
->set_label( __( 'Settings', 'elementor' ) )
->set_id( 'settings' )
->set_items( [] ),
];
}
protected function define_base_styles(): array {
$display = String_Prop_Type::generate( 'block' );
$padding = Size_Prop_Type::generate( [
'size' => 4,
'unit' => 'px',
] );
return [
static::BASE_STYLE_KEY => Style_Definition::make()
->add_variant(
Style_Variant::make()
->add_prop( 'display', $display )
->add_prop( 'padding', $padding )
),
];
}
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_Heading::generate()
->settings( [
'title' => String_Prop_Type::generate( 'Tab' ),
] )
->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();
$attributes = [
'class' => [
'e-con',
'e-atomic-element',
$base_style_class,
...( $settings['classes'] ?? [] ),
],
];
if ( ! empty( $settings['tab-panel-id'] ) ) {
$attributes['aria-controls'] = esc_attr( $settings['tab-panel-id'] );
}
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,117 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Atomic_Tabs;
use Elementor\Modules\AtomicWidgets\Elements\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;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Atomic_Tabs_Content extends Atomic_Element_Base {
const BASE_STYLE_KEY = 'base';
public static function get_type() {
return 'e-tabs-content';
}
public static function get_element_type(): string {
return 'e-tabs-content';
}
public function get_title() {
return esc_html__( 'Atomic Tabs Content', 'elementor' );
}
public function get_keywords() {
return [ 'ato', 'atom', 'atoms', 'atomic' ];
}
public function get_icon() {
return 'eicon-tabs';
}
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(),
];
}
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( $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'] );
}
$this->add_render_attribute( '_wrapper', array_merge( $initial_attributes, $attributes ) );
}
}

View File

@@ -0,0 +1,39 @@
import { register } from '@elementor/frontend-handlers';
register( {
elementType: 'e-tabs',
uniqueId: 'e-tabs-handler',
callback: ( { element, signal } ) => {
const tabs = element.querySelectorAll( '[data-element_type="e-tab"]' );
const tabPanels = element.querySelectorAll( '[data-element_type="e-tab-panel"]' );
const setActiveTab = ( id ) => {
tabPanels.forEach( ( tabPanel ) => {
const activeTab = tabPanel.getAttribute( 'data-tab-id' ) === id;
if ( activeTab ) {
tabPanel.style.removeProperty( 'display' );
tabPanel.removeAttribute( 'hidden' );
return;
}
tabPanel.style.display = 'none';
tabPanel.setAttribute( 'hidden', 'true' );
} );
};
const defaultActiveTab = element.getAttribute( 'data-active-tab' );
setActiveTab( defaultActiveTab );
tabs.forEach( ( tab ) => {
const clickHandler = () => {
const tabId = tab.getAttribute( 'data-id' );
setActiveTab( tabId );
};
tab.addEventListener( 'click', clickHandler, { signal } );
} );
},
} );

View File

@@ -0,0 +1,111 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Atomic_Tabs;
use Elementor\Modules\AtomicWidgets\Elements\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\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;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Atomic_Tabs_List extends Atomic_Element_Base {
const BASE_STYLE_KEY = 'base';
public static function get_type() {
return 'e-tabs-list';
}
public static function get_element_type(): string {
return 'e-tabs-list';
}
public function get_title() {
return esc_html__( 'Atomic Tabs List', 'elementor' );
}
public function get_keywords() {
return [ 'ato', 'atom', 'atoms', 'atomic' ];
}
public function get_icon() {
return 'eicon-tabs';
}
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(),
];
}
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( $this->get_css_id_control_meta() ),
] ),
];
}
protected function define_base_styles(): array {
$display = String_Prop_Type::generate( 'flex' );
$gap = Size_Prop_Type::generate( [
'size' => 10,
'unit' => 'px',
] );
$justify_content = String_Prop_Type::generate( 'space-between' );
$min_width = Size_Prop_Type::generate( [
'size' => 30,
'unit' => 'px',
] );
return [
static::BASE_STYLE_KEY => Style_Definition::make()
->add_variant(
Style_Variant::make()
->add_prop( 'display', $display )
->add_prop( 'min-width', $min_width )
->add_prop( 'gap', $gap )
->add_prop( 'justify-content', $justify_content )
),
];
}
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,171 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Atomic_Tabs;
use Elementor\Modules\AtomicWidgets\Elements\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\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\Controls\Types\Elements\Tabs_Control;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Atomic_Tabs extends Atomic_Element_Base {
const BASE_STYLE_KEY = 'base';
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__( 'Atomic 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' => String_Prop_Type::make(),
'attributes' => Attributes_Prop_Type::make(),
];
}
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( $this->get_css_id_control_meta() ),
Tabs_Control::make()
->set_label( __( 'Menu items', 'elementor' ) )
->set_meta( [
'topDivider' => true,
'layout' => 'custom',
] ),
] ),
];
}
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 define_default_children() {
$default_tab_count = 3;
$tab_elements = [];
$tab_panel_elements = [];
foreach ( range( 1, $default_tab_count ) as $i ) {
$tab_elements[] = Atomic_Tab::generate()
->editor_settings( [
'title' => "Tab {$i}",
] )
->is_locked( true )
->build();
$tab_panel_elements[] = Atomic_Tab_Panel::generate()
->is_locked( true )
->editor_settings( [
'title' => "Tab {$i} panel",
] )
->build();
}
$tabs_list = Atomic_Tabs_List::generate()
->children( $tab_elements )
->is_locked( true )
->build();
$tabs_content = Atomic_Tabs_Content::generate()
->children( $tab_panel_elements )
->is_locked( true )
->build();
return [
$tabs_list,
$tabs_content,
];
}
public function define_initial_attributes() {
return [
'data-e-type' => $this->get_type(),
];
}
public function get_script_depends() {
return [ 'elementor-tabs-handler' ];
}
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['default-active-tab'] ) ) {
$attributes['data-active-tab'] = esc_attr( $settings['default-active-tab'] );
}
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,65 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements;
use Elementor\Modules\AtomicWidgets\PropDependencies\Manager as Dependency_Manager;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Prop_Type;
use Elementor\Widget_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Atomic_Widget_Base extends Widget_Base {
use Has_Atomic_Base;
protected $version = '0.0';
protected $styles = [];
protected $editor_settings = [];
public function __construct( $data = [], $args = null ) {
parent::__construct( $data, $args );
$this->version = $data['version'] ?? '0.0';
$this->styles = $data['styles'] ?? [];
$this->editor_settings = $data['editor_settings'] ?? [];
}
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;
return $config;
}
public function get_categories(): array {
return [ 'v4-elements' ];
}
/**
* TODO: Removes the wrapper div from the widget.
*/
public function before_render() {}
public function after_render() {}
/**
* @return array<string, Prop_Type>
*/
abstract protected static function define_props_schema(): array;
public static function generate() {
return Widget_Builder::make( static::get_element_type() );
}
}

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-e-type="{{ type }}" {{ id_attribute }} class="{{ classes }}" {{ settings.attributes | raw }} data-settings="{{ data_settings|json_encode|e('html_attr') }}"></div>
{% endif %}

View File

@@ -0,0 +1,124 @@
<?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\Elements\Atomic_Widget_Base;
use Elementor\Modules\AtomicWidgets\Elements\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;
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(),
'end' => String_Prop_Type::make(),
'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(),
];
}
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 [ 'elementor-youtube-handler' ];
}
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',
uniqueId: '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,172 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Div_Block;
use Elementor\Modules\AtomicWidgets\Elements\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;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Div_Block extends Atomic_Element_Base {
const BASE_STYLE_KEY = 'base';
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()
->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' ] )
->default( 'div' )
->set_dependencies( $tag_dependencies ),
'link' => Link_Prop_Type::make(),
'attributes' => Attributes_Prop_Type::make(),
];
}
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'] ) ) {
$attributes = array_merge( $attributes, $settings['link'] );
}
$this->add_render_attribute( '_wrapper', array_merge( $initial_attributes, $attributes ) );
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements;
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,166 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Flexbox;
use Elementor\Modules\AtomicWidgets\Elements\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\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\PropDependencies\Manager as Dependency_Manager;
use Elementor\Modules\AtomicWidgets\PropTypes\Classes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Attributes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Link_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Flexbox extends Atomic_Element_Base {
const BASE_STYLE_KEY = 'base';
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()
->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' ] )
->default( 'div' )
->set_dependencies( $tag_dependencies ),
'link' => Link_Prop_Type::make(),
'attributes' => Attributes_Prop_Type::make(),
];
}
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'] ) ) {
$attributes = array_merge( $attributes, $settings['link'] );
}
$this->add_render_attribute( '_wrapper', array_merge( $initial_attributes, $attributes ) );
}
}

View File

@@ -0,0 +1,202 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements;
use Elementor\Element_Base;
use Elementor\Modules\AtomicWidgets\Base\Atomic_Control_Base;
use Elementor\Modules\AtomicWidgets\Base\Element_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\Primitives\String_Prop_Type;
use Elementor\Utils;
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 Element_Control_Base ) {
$valid_controls[] = $control;
continue;
}
if ( ! ( $control instanceof Atomic_Control_Base ) ) {
Utils::safe_throw( 'Control must be an instance of `Atomic_Control_Base`.' );
continue;
}
$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 $styles ): array {
$style_parser = Style_Parser::make( Style_Schema::get() );
foreach ( $styles as $style_id => $style ) {
$result = $style_parser->parse( $style );
if ( ! $result->is_valid() ) {
throw new \Exception( esc_html( "Styles validation failed for style `$style_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();
}
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['styles'] );
$data['editor_settings'] = $this->parse_editor_settings( $data['editor_settings'] );
return $data;
}
final public function get_raw_data( $with_html_content = false ) {
$raw_data = parent::get_raw_data( $with_html_content );
$raw_data['styles'] = $this->styles;
$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();
return Render_Props_Resolver::for_settings()->resolve( $schema, $props );
}
private 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();
return apply_filters(
'elementor/atomic-widgets/props-schema',
$schema
);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements;
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,79 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements;
use Elementor\Modules\AtomicWidgets\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(),
];
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $renderer->render( $this->get_main_template(), $context );
} catch ( \Exception $e ) {
if ( Utils::is_elementor_debug() ) {
throw $e;
}
}
}
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,45 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements;
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,66 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Image;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Image_Sizes {
public static function get_keys() {
return array_map(
fn( $size ) => $size['value'],
static::get_all()
);
}
public static function get_all(): array {
$wp_image_sizes = static::get_wp_image_sizes();
$image_sizes = [];
foreach ( $wp_image_sizes as $size_key => $size_attributes ) {
$control_title = ucwords( str_replace( '_', ' ', $size_key ) );
if ( is_array( $size_attributes ) ) {
$control_title .= sprintf( ' - %d*%d', $size_attributes['width'], $size_attributes['height'] );
}
$image_sizes[] = [
'label' => $control_title,
'value' => $size_key,
];
}
$image_sizes[] = [
'label' => esc_html__( 'Full', 'elementor' ),
'value' => 'full',
];
return $image_sizes;
}
private static function get_wp_image_sizes() {
$default_image_sizes = get_intermediate_image_sizes();
$additional_sizes = wp_get_additional_image_sizes();
$image_sizes = [];
foreach ( $default_image_sizes as $size ) {
$image_sizes[ $size ] = [
'width' => (int) get_option( $size . '_size_w' ),
'height' => (int) get_option( $size . '_size_h' ),
'crop' => (bool) get_option( $size . '_crop' ),
];
}
if ( $additional_sizes ) {
$image_sizes = array_merge( $image_sizes, $additional_sizes );
}
// /** This filter is documented in wp-admin/includes/media.php */
return apply_filters( 'image_size_names_choose', $image_sizes );
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Image;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Placeholder_Image {
public static function get_placeholder_image() {
return ELEMENTOR_ASSETS_URL . 'images/placeholder-v4.svg';
}
public static function get_background_placeholder_image() {
return ELEMENTOR_ASSETS_URL . 'images/background-placeholder.svg';
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Elementor\Modules\AtomicWidgets\ImportExport;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Element_Base;
use Elementor\Modules\AtomicWidgets\Elements\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;
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;
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,22 @@
<?php
namespace Elementor\Modules\AtomicWidgets;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Memo {
private array $cache = [];
public function memoize( string $key, callable $callback ) {
return function() use ( $key, $callback ) {
if ( array_key_exists( $key, $this->cache ) ) {
return $this->cache[ $key ];
}
$this->cache[ $key ] = call_user_func( $callback );
return $this->cache[ $key ];
};
}
}

View File

@@ -0,0 +1,412 @@
<?php
namespace Elementor\Modules\AtomicWidgets;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Experiments\Manager as Experiments_Manager;
use Elementor\Core\Utils\Assets_Config_Provider;
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_List;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Tabs\Atomic_Tab;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Tabs\Atomic_Tabs_Content;
use Elementor\Modules\AtomicWidgets\ImportExport\Atomic_Import_Export;
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\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\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_Panel;
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\Query_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Transform\Perspective_Origin_Prop_Type;
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_NESTED = 'e_nested_elements';
const EXPERIMENT_EDITOR_MCP = 'editor_mcp';
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',
];
public function get_name() {
return 'atomic-widgets';
}
public function __construct() {
parent::__construct();
if ( self::is_active() ) {
$this->register_experimental_features();
}
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_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/after_register_scripts', fn () => $this->register_frontend_scripts() );
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_NESTED,
'title' => esc_html__( 'Nested Elements', 'elementor' ),
'description' => esc_html__( 'Enable nested elements.', 'elementor' ),
'hidden' => true,
'default' => Experiments_Manager::STATE_INACTIVE,
'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_INACTIVE,
'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() );
if ( Plugin::$instance->experiments->is_feature_active( self::EXPERIMENT_NESTED ) ) {
$elements_manager->register_element_type( new Atomic_Tabs() );
$elements_manager->register_element_type( new Atomic_Tabs_List() );
$elements_manager->register_element_type( new Atomic_Tab() );
$elements_manager->register_element_type( new Atomic_Tabs_Content() );
$elements_manager->register_element_type( new Atomic_Tab_Panel() );
}
}
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() );
}
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__( 'Alpha', 'elementor' ); ?><i class="eicon-info"></i>
<span class="e-promotion-react-wrapper" data-promotion="v4_chip"></span>
</span>
<# } #><?php
}
private function register_frontend_scripts() {
$assets_config_provider = ( new Assets_Config_Provider() )
->set_path_resolver( function ( $name ) {
return ELEMENTOR_ASSETS_PATH . "js/packages/{$name}/{$name}.asset.php";
} );
$assets_config_provider->load( 'frontend-handlers' );
$frontend_handlers_package_config = $assets_config_provider->get( 'frontend-handlers' );
if ( ! $frontend_handlers_package_config ) {
return;
}
wp_register_script(
$frontend_handlers_package_config['handle'],
$this->get_js_assets_url( 'packages/frontend-handlers/frontend-handlers' ),
$frontend_handlers_package_config['deps'],
ELEMENTOR_VERSION,
true
);
wp_register_script(
'elementor-youtube-handler',
$this->get_js_assets_url( 'youtube-handler' ),
[ $frontend_handlers_package_config['handle'] ],
ELEMENTOR_VERSION,
true
);
wp_register_script(
'elementor-tabs-handler',
$this->get_js_assets_url( 'tabs-handler' ),
[ $frontend_handlers_package_config['handle'] ],
ELEMENTOR_VERSION,
true
);
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace Elementor\Modules\AtomicWidgets;
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\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,
];
const OPT_IN_FEATURES = [
self::EXPERIMENT_NAME,
'container',
NestedElementsModule::EXPERIMENT_NAME,
AtomicWidgetsModule::EXPERIMENT_NAME,
GlobalClassesModule::NAME,
VariablesModule::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() );
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Parsers;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Prop_Type;
use Elementor\Core\Utils\Api\Parse_Result;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Props_Parser {
private array $schema;
public function __construct( array $schema ) {
$this->schema = $schema;
}
public static function make( array $schema ): self {
return new static( $schema );
}
/**
* @param array $props
* The key of each item represents the prop name (should match the schema),
* and the value is the prop value to validate
*/
public function validate( array $props ): Parse_Result {
$result = Parse_Result::make();
$validated = [];
foreach ( $this->schema as $key => $prop_type ) {
if ( ! ( $prop_type instanceof Prop_Type ) ) {
continue;
}
$value = $props[ $key ] ?? null;
$is_valid = $prop_type->validate( $value ?? $prop_type->get_default() );
if ( ! $is_valid ) {
$result->errors()->add( $key, 'invalid_value' );
continue;
}
if ( ! is_null( $value ) ) {
$validated[ $key ] = $value;
}
}
return $result->wrap( $validated );
}
/**
* @param array $props
* The key of each item represents the prop name (should match the schema),
* and the value is the prop value to sanitize
*/
public function sanitize( array $props ): Parse_Result {
$sanitized = [];
foreach ( $this->schema as $key => $prop_type ) {
if ( ! isset( $props[ $key ] ) ) {
continue;
}
$sanitized[ $key ] = $prop_type->sanitize( $props[ $key ] );
}
return Parse_Result::make()->wrap( $sanitized );
}
/**
* @param array $props
* The key of each item represents the prop name (should match the schema),
* and the value is the prop value to parse
*/
public function parse( array $props ): Parse_Result {
$validate_result = $this->validate( $props );
$sanitize_result = $this->sanitize( $validate_result->unwrap() );
$sanitize_result->errors()->merge( $validate_result->errors() );
return $sanitize_result;
}
}

View File

@@ -0,0 +1,264 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Parsers;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use Elementor\Modules\AtomicWidgets\Opt_In;
use Elementor\Plugin;
use Elementor\Utils;
use Elementor\Core\Utils\Api\Parse_Result;
class Style_Parser {
const VALID_TYPES = [
'class',
];
const VALID_STATES = [
'hover',
'active',
'focus',
null,
];
private array $schema;
public function __construct( array $schema ) {
$this->schema = $schema;
}
public static function make( array $schema ): self {
return new static( $schema );
}
/**
* @param array $style
* the style object to validate
*/
private function validate( array $style ): Parse_Result {
$validated_style = $style;
$result = Parse_Result::make();
if ( ! isset( $style['id'] ) || ! is_string( $style['id'] ) ) {
$result->errors()->add( 'id', 'missing_or_invalid' );
}
if ( ! isset( $style['type'] ) || ! in_array( $style['type'], self::VALID_TYPES, true ) ) {
$result->errors()->add( 'type', 'missing_or_invalid' );
}
if ( ! isset( $style['label'] ) || ! is_string( $style['label'] ) ) {
$result->errors()->add( 'label', 'missing_or_invalid' );
} elseif ( Plugin::$instance->experiments->is_feature_active( Opt_In::EXPERIMENT_NAME ) ) {
$label_validation = $this->validate_style_label( $style['label'] );
if ( ! $label_validation['is_valid'] ) {
$result->errors()->add( 'label', $label_validation['error_message'] );
}
}
if ( ! isset( $style['variants'] ) || ! is_array( $style['variants'] ) ) {
$result->errors()->add( 'variants', 'missing_or_invalid' );
unset( $validated_style['variants'] );
return $result->wrap( $validated_style );
}
$props_parser = Props_Parser::make( $this->schema );
foreach ( $style['variants'] as $variant_index => $variant ) {
if ( ! isset( $variant['meta'] ) ) {
$result->errors()->add( 'meta', 'missing' );
continue;
}
$meta_result = $this->validate_meta( $variant['meta'] );
$custom_css_result = $this->validate_custom_css( $variant );
$result->errors()->merge( $meta_result->errors(), 'meta' );
$result->errors()->merge( $custom_css_result->errors(), 'custom_css' );
if ( $meta_result->is_valid() ) {
$variant_result = $props_parser->validate( $variant['props'] );
$result->errors()->merge( $variant_result->errors(), "variants[$variant_index]" );
$validated_style['variants'][ $variant_index ]['props'] = $variant_result->unwrap();
} else {
unset( $validated_style['variants'][ $variant_index ] );
}
}
return $result->wrap( $validated_style );
}
private function validate_style_label( string $label ): array {
$label = strtolower( $label );
$reserved_class_names = [ 'container' ];
if ( strlen( $label ) > 50 ) {
return [
'is_valid' => false,
'error_message' => 'class_name_too_long',
];
}
if ( strlen( $label ) < 2 ) {
return [
'is_valid' => false,
'error_message' => 'class_name_too_short',
];
}
if ( in_array( $label, $reserved_class_names, true ) ) {
return [
'is_valid' => false,
'error_message' => 'reserved_class_name',
];
}
$regexes = [
[
'pattern' => '/^(|[^0-9].*)$/',
'message' => 'class_name_starts_with_digit',
],
[
'pattern' => '/^\S*$/',
'message' => 'class_name_contains_spaces',
],
[
'pattern' => '/^(|[a-zA-Z0-9_-]+)$/',
'message' => 'class_name_invalid_chars',
],
[
'pattern' => '/^(?!--).*/',
'message' => 'class_name_double_hyphen',
],
[
'pattern' => '/^(?!-[0-9])/',
'message' => 'class_name_starts_with_hyphen_digit',
],
];
foreach ( $regexes as $rule ) {
if ( ! preg_match( $rule['pattern'], $label ) ) {
return [
'is_valid' => false,
'error_message' => $rule['message'],
];
}
}
return [
'is_valid' => true,
'error_message' => null,
];
}
private function validate_meta( $meta ): Parse_Result {
$result = Parse_Result::make();
if ( ! is_array( $meta ) ) {
$result->errors()->add( 'meta', 'invalid_type' );
return $result;
}
if ( ! array_key_exists( 'state', $meta ) || ! in_array( $meta['state'], self::VALID_STATES, true ) ) {
$result->errors()->add( 'state', 'missing_or_invalid_value' );
return $result;
}
// TODO: Validate breakpoint based on the existing breakpoints in the system [EDS-528]
if ( ! isset( $meta['breakpoint'] ) || ! is_string( $meta['breakpoint'] ) ) {
$result->errors()->add( 'breakpoint', 'missing_or_invalid_value' );
return $result;
}
return $result;
}
private function validate_custom_css( array $variant ): Parse_Result {
$result = Parse_Result::make();
if ( ! empty( $variant['custom_css']['raw'] ) && (
! is_string( $variant['custom_css']['raw'] ) ||
null === Utils::decode_string( $variant['custom_css']['raw'], null )
)
) {
$result->errors()->add( 'custom_css', 'invalid_type' );
}
return $result;
}
private function sanitize_meta( $meta ) {
if ( ! is_array( $meta ) ) {
return [];
}
if ( isset( $meta['breakpoint'] ) ) {
$meta['breakpoint'] = sanitize_key( $meta['breakpoint'] );
}
return $meta;
}
private function sanitize_custom_css( array $variant ) {
if ( empty( $variant['custom_css']['raw'] ) ) {
return null;
}
$custom_css = Utils::decode_string( $variant['custom_css']['raw'] );
$custom_css = sanitize_textarea_field( $custom_css );
$custom_css = [ 'raw' => Utils::encode_string( $custom_css ) ];
return empty( $custom_css['raw'] ) ? null : $custom_css;
}
/**
* @param array $style
* the style object to sanitize
*/
private function sanitize( array $style ): Parse_Result {
$props_parser = Props_Parser::make( $this->schema );
if ( isset( $style['label'] ) ) {
$style['label'] = sanitize_text_field( $style['label'] );
}
if ( isset( $style['id'] ) ) {
$style['id'] = sanitize_key( $style['id'] );
}
if ( ! empty( $style['variants'] ) ) {
foreach ( $style['variants'] as $variant_index => $variant ) {
$style['variants'][ $variant_index ]['props'] = $props_parser->sanitize( $variant['props'] )->unwrap();
$style['variants'][ $variant_index ]['meta'] = $this->sanitize_meta( $variant['meta'] );
$style['variants'][ $variant_index ]['custom_css'] = $this->sanitize_custom_css( $variant );
}
}
return Parse_Result::make()->wrap( $style );
}
/**
* @param array $style
* the style object to parse
*/
public function parse( array $style ): Parse_Result {
$validate_result = $this->validate( $style );
$sanitize_result = $this->sanitize( $validate_result->unwrap() );
$sanitize_result->errors()->merge( $validate_result->errors() );
return $sanitize_result;
}
}

View File

@@ -0,0 +1,240 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropDependencies;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Prop_Type;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Manager {
const RELATION_OR = 'or';
const RELATION_AND = 'and';
const OPERATORS = [
'lt',
'lte',
'eq',
'ne',
'gte',
'gt',
'exists',
'not_exist',
'in',
'nin',
'contains',
'ncontains',
];
/**
* @var ?array{
* relation: self::RELATION_OR|self::RELATION_AND,
* terms: array{
* operator: string,
* path: array<string>,
* value?: mixed,
* newValue?: array
* }
* }
*/
private ?array $dependencies;
public function __construct( string $relation = self::RELATION_OR ) {
$this->new( $relation );
return $this;
}
public static function make( string $relation = self::RELATION_OR ): self {
return new self( $relation );
}
/**
* @param array<string, Prop_Type> $props_schema
* @return array<string, array<string>> Returns source prop path => array of dependent prop paths
*/
public static function get_source_to_dependents( array $props_schema ): array {
$dependency_graph = self::build_dependency_graph( $props_schema );
if ( self::has_circular_dependencies( $dependency_graph ) ) {
Utils::safe_throw( 'Circular prop dependencies detected' );
}
return $dependency_graph;
}
/**
* @param $config array{
* operator: string,
* path: array<string>,
* value?: mixed,
* newValue?: array,
* }
* @return self
*/
public function where( array $config ): self {
if ( ! isset( $config['operator'] ) || ! isset( $config['path'] ) ) {
Utils::safe_throw( 'Term missing mandatory configurations' );
}
if ( ! in_array( $config['operator'], self::OPERATORS, true ) ) {
Utils::safe_throw( "Invalid operator: {$config['operator']}." );
}
$term = [
'operator' => $config['operator'],
'path' => $config['path'],
'value' => $config['value'] ?? null,
'newValue' => $config['newValue'] ?? null,
];
if ( empty( $this->dependencies ) ) {
$this->new();
}
$this->dependencies['terms'][] = $term;
return $this;
}
private function new( string $relation = self::RELATION_OR ): self {
if ( ! in_array( $relation, [ self::RELATION_OR, self::RELATION_AND ], true ) ) {
Utils::safe_throw( "Invalid relation: $relation. Must be one of: " . implode( ', ', [ self::RELATION_OR, self::RELATION_AND ] ) );
}
$this->dependencies = [
'relation' => $relation,
'terms' => [],
];
return $this;
}
public function get(): ?array {
return empty( $this->dependencies['terms'] ?? [] ) ? null : $this->dependencies;
}
/**
* @param array<string, Prop_Type> $props_schema The props schema to analyze, where keys are prop names
* @param ?array<string> $current_path The current property path being processed
* @param ?array<string, array<string>> $dependency_graph The dependency graph to build
*/
private static function build_dependency_graph( array $props_schema, ?array $current_path = [], ?array $dependency_graph = [] ): array {
foreach ( $props_schema as $prop_name => $prop_type ) {
$dependency_graph = self::build_nested_prop_dependency_graph( $prop_name, $prop_type, $current_path, $dependency_graph );
$dependencies = $prop_type->get_dependencies();
if ( ! $dependencies ) {
continue;
}
foreach ( $dependencies['terms'] as $term ) {
$dependency_graph = self::process_dependency_term( $term, $current_path, $prop_name, $dependency_graph );
}
}
return $dependency_graph;
}
private static function build_nested_prop_dependency_graph( string $prop_name, Prop_Type $prop_type, array $current_path, array $dependency_graph ): array {
$nested_prop_path = array_merge( $current_path, [ $prop_name ] );
switch ( $prop_type->get_type() ) {
case 'object':
foreach ( $prop_type->get_shape() as $nested_prop_name => $nested_prop_type ) {
$dependency_graph = self::build_dependency_graph( [ $nested_prop_name => $nested_prop_type ], $nested_prop_path, $dependency_graph );
}
break;
case 'array':
$item_prop_type = $prop_type->get_item_type();
$dependency_graph = self::build_dependency_graph( [ $prop_name => $item_prop_type ], $current_path, $dependency_graph );
break;
case 'union':
foreach ( $prop_type->get_prop_types() as $nested_prop_type ) {
$dependency_graph = self::build_dependency_graph( [ $prop_name => $nested_prop_type ], $current_path, $dependency_graph );
}
break;
}
return $dependency_graph;
}
private static function process_dependency_term( array $term, array $current_path, string $prop_name, array $dependency_graph ): array {
if ( self::is_term_nested( $term ) ) {
foreach ( $term['terms'] as $nested_term ) {
$dependency_graph = self::process_dependency_term( $nested_term, $current_path, $prop_name, $dependency_graph );
}
return $dependency_graph;
}
if ( ! isset( $term['path'] ) || empty( $term['path'] ) ) {
Utils::safe_throw( 'Invalid term path in dependency.' );
}
$target_path = implode( '.', $term['path'] );
$source = array_merge( $current_path, [ $prop_name ] );
$source_path = implode( '.', $source );
if ( ! isset( $dependency_graph[ $target_path ] ) ) {
$dependency_graph[ $target_path ] = [];
}
if ( ! in_array( $source_path, $dependency_graph[ $target_path ] ) ) {
$dependency_graph[ $target_path ][] = $source_path;
}
return $dependency_graph;
}
private static function has_circular_dependencies( array $dependency_graph ): bool {
$visited_nodes = [];
$current_path_stack = [];
foreach ( array_keys( $dependency_graph ) as $node ) {
if ( isset( $visited_nodes[ $node ] ) ) {
continue;
}
if ( self::detect_cycle_from_node( $dependency_graph, $node, $visited_nodes, $current_path_stack ) ) {
return true;
}
}
return false;
}
private static function detect_cycle_from_node( array $dependency_graph, string $current_node, array &$visited_nodes, array &$current_path_stack ): bool {
if ( isset( $current_path_stack[ $current_node ] ) ) {
return true;
}
if ( isset( $visited_nodes[ $current_node ] ) ) {
return false;
}
$visited_nodes[ $current_node ] = true;
$current_path_stack[ $current_node ] = true;
foreach ( $dependency_graph[ $current_node ] ?? [] as $dependent_node ) {
$is_circular = self::detect_cycle_from_node( $dependency_graph, $dependent_node, $visited_nodes, $current_path_stack );
if ( $is_circular ) {
return true;
}
}
unset( $current_path_stack[ $current_node ] );
return false;
}
private static function is_term_nested( $term ): bool {
return isset( $term['terms'] ) && is_array( $term['terms'] );
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Array_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Attributes_Prop_Type extends Array_Prop_Type {
public static function get_key(): string {
return 'attributes';
}
protected function define_item_type(): Prop_Type {
return Key_Value_Prop_Type::make();
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Object_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Background_Color_Overlay_Prop_Type extends Object_Prop_Type {
public static function get_key(): string {
return 'background-color-overlay';
}
protected function define_shape(): array {
return [
'color' => Color_Prop_Type::make(),
];
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Object_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\Position_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Background_Gradient_Overlay_Prop_Type extends Object_Prop_Type {
public static function get_key(): string {
return 'background-gradient-overlay';
}
protected function define_shape(): array {
return [
'type' => String_Prop_Type::make()->enum( [ 'linear', 'radial' ] ),
'angle' => Number_Prop_Type::make(),
'stops' => Gradient_Color_Stop_Prop_Type::make(),
'positions' => String_Prop_Type::make()->enum( self::get_position_enum_values() ),
];
}
private static function get_position_enum_values(): array {
return Position_Prop_Type::get_position_enum_values();
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Object_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Position_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Background_Image_Overlay_Prop_Type extends Object_Prop_Type {
public static function get_key(): string {
return 'background-image-overlay';
}
protected function define_shape(): array {
return [
'image' => Image_Prop_Type::make(),
'repeat' => String_Prop_Type::make()->enum( [ 'repeat', 'repeat-x', 'repeat-y', 'no-repeat' ] ),
'size' => Union_Prop_Type::make()
->add_prop_type( String_Prop_Type::make()->enum( [ 'auto', 'cover', 'contain' ] ) )
->add_prop_type( Background_Image_Overlay_Size_Scale_Prop_Type::make() ),
'position' => Union_Prop_Type::make()
->add_prop_type( String_Prop_Type::make()->enum( Position_Prop_Type::get_position_enum_values() ) )
->add_prop_type( Background_Image_Position_Offset_Prop_Type::make() ),
'attachment' => String_Prop_Type::make()->enum( [ 'fixed', 'scroll' ] ),
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Object_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Background_Image_Overlay_Size_Scale_Prop_Type extends Object_Prop_Type {
public static function get_key(): string {
return 'background-image-size-scale';
}
protected function define_shape(): array {
return [
'width' => Size_Prop_Type::make(),
'height' => Size_Prop_Type::make(),
];
}
}

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