Initial commit: Atomaste website

This commit is contained in:
2025-12-10 12:17:30 -05:00
commit 0b9e5d1605
19260 changed files with 5206382 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', null, 'default', true ), [], ELEMENTOR_VERSION );
/**
* Before admin top bar enqueue scripts.
*
* Fires before Elementor admin top bar scripts are enqueued.
*
* @since 3.19.0
*/
do_action( 'elementor/admin_top_bar/before_enqueue_scripts', $this );
wp_enqueue_script( 'elementor-admin-top-bar', $this->get_js_assets_url( 'admin-top-bar' ), [
'elementor-common',
'react',
'react-dom',
'tipsy',
], ELEMENTOR_VERSION, true );
wp_set_script_translations( 'elementor-admin-top-bar', 'elementor' );
$min_suffix = Utils::is_script_debug() ? '' : '.min';
wp_enqueue_script( 'tipsy', ELEMENTOR_ASSETS_URL . 'lib/tipsy/tipsy' . $min_suffix . '.js', [
'jquery',
], '1.0.0', true );
$this->print_config();
}
private function add_frontend_settings() {
$settings = [];
$settings['is_administrator'] = current_user_can( 'manage_options' );
// TODO: Find a better way to add apps page url to the admin top bar.
$settings['apps_url'] = admin_url( 'admin.php?page=elementor-apps' );
$settings['promotion'] = [
'text' => __( 'Upgrade Now', 'elementor' ),
'url' => 'https://go.elementor.com/wp-dash-admin-top-bar-upgrade/',
];
$settings['promotion'] = Filtered_Promotions_Manager::get_filtered_promotion_data(
$settings['promotion'],
'elementor/admin_top_bar/go_pro_promotion',
'url'
);
$current_screen = get_current_screen();
/** @var \Elementor\Core\Common\Modules\Connect\Apps\Library $library */
$library = Plugin::$instance->common->get_component( 'connect' )->get_app( 'library' );
if ( $library ) {
$settings = array_merge( $settings, [
'is_user_connected' => $library->is_connected(),
'connect_url' => $library->get_admin_url( 'authorize', [
'utm_source' => 'top-bar',
'utm_medium' => 'wp-dash',
'utm_campaign' => 'connect-account',
'utm_content' => $current_screen->id,
'source' => 'generic',
] ),
] );
}
$this->set_settings( $settings );
do_action( 'elementor/admin-top-bar/init', $this );
}
private function is_top_bar_active() {
$current_screen = get_current_screen();
if ( ! $current_screen ) {
return false;
}
$is_elementor_page = strpos( $current_screen->id ?? '', 'elementor' ) !== false;
$is_elementor_post_type_page = strpos( $current_screen->post_type ?? '', 'elementor' ) !== false;
return apply_filters(
'elementor/admin-top-bar/is-active',
$is_elementor_page || $is_elementor_post_type_page,
$current_screen
);
}
/**
* Module constructor.
*/
public function __construct() {
parent::__construct();
add_action( 'current_screen', function () {
if ( ! $this->is_top_bar_active() ) {
return;
}
$this->add_frontend_settings();
add_action( 'in_admin_header', function () {
$this->render_admin_top_bar();
} );
add_action( 'admin_enqueue_scripts', function () {
$this->enqueue_scripts();
} );
} );
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,194 @@
<?php
namespace Elementor\Modules\Apps;
use Elementor\Core\Isolation\Wordpress_Adapter;
use Elementor\Core\Isolation\Plugin_Status_Adapter;
use Elementor\Plugin;
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 {
$container = Plugin::$instance->elementor_container();
if ( $container->has( Wordpress_Adapter::class ) ) {
self::$wordpress_adapter = $container->get( Wordpress_Adapter::class );
} else 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,56 @@
<?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;
abstract public function get_type(): string;
abstract public function get_props(): array;
public static function bind_to( string $prop_name ) {
return new static( $prop_name );
}
protected function __construct( string $prop_name ) {
$this->bind = $prop_name;
}
public function get_bind() {
return $this->bind;
}
public function set_label( string $label ): self {
$this->label = $label;
return $this;
}
public function set_description( string $description ): self {
$this->description = $description;
return $this;
}
public function jsonSerialize(): array {
return [
'type' => 'control',
'value' => [
'type' => $this->get_type(),
'bind' => $this->get_bind(),
'label' => $this->label,
'description' => $this->description,
'props' => $this->get_props(),
],
];
}
}

View File

@@ -0,0 +1,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,57 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls;
use JsonSerializable;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Section implements JsonSerializable {
private $label = null;
private $description = null;
private array $items = [];
public static function make(): self {
return new static();
}
public function set_label( string $label ): self {
$this->label = $label;
return $this;
}
public function set_description( string $description ): self {
$this->description = $description;
return $this;
}
public function set_items( array $items ): self {
$this->items = $items;
return $this;
}
public function 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,22 @@
<?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 {
public function get_type(): string {
return 'image';
}
public function get_props(): array {
return [
'sizes' => Image_Sizes::get_all(),
];
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls\Types;
use Elementor\Modules\AtomicWidgets\Base\Atomic_Control_Base;
use Elementor\Modules\WpRest\Classes\WP_Post;
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 ?string $placeholder = null;
private array $query_options = [
'endpoint' => '',
'requestParams' => [],
];
public static function bind_to( string $prop_name ) {
$instance = parent::bind_to( $prop_name );
$instance->set_placeholder( __( 'Paste URL or type', 'elementor' ) );
$instance->set_endpoint( WP_Post::ENDPOINT );
$instance->set_request_params( WP_Post::build_query_params( [
WP_Post::KEYS_FORMAT_MAP_KEY => [
'ID' => 'id',
'post_title' => 'label',
'post_type' => 'groupLabel',
],
] ) );
return $instance;
}
public function get_type(): string {
return 'link';
}
public function set_placeholder( string $placeholder ): self {
$this->placeholder = $placeholder;
return $this;
}
public function get_props(): array {
return [
'placeholder' => $this->placeholder,
'allowCustomValues' => $this->allow_custom_values,
'queryOptions' => $this->query_options,
'minInputLength' => $this->minimum_input_length,
];
}
public function set_allow_custom_values( bool $allow_custom_values ): self {
$this->allow_custom_values = $allow_custom_values;
return $this;
}
public function set_endpoint( string $url ): self {
$this->query_options['endpoint'] = $url;
return $this;
}
public function set_request_params( array $params ): self {
$this->query_options['requestParams'] = $params;
return $this;
}
public function set_minimum_input_length( int $input_length ): self {
$this->minimum_input_length = $input_length;
return $this;
}
}

View File

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

View File

@@ -0,0 +1,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,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 = $placeholder;
return $this;
}
public function get_props(): array {
return [
'placeholder' => $this->placeholder,
];
}
}

View File

@@ -0,0 +1,80 @@
<?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;
}
[ $is_valid ] = Props_Parser::make( $tag['props_schema'] )->validate( $value['settings'] );
return $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'] );
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,117 @@
<?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;
}
$union_prop_type = $prop_type instanceof Transformable_Prop_Type ?
Union_Prop_Type::create_from( $prop_type ) :
$prop_type;
$union_prop_type->add_prop_type(
Dynamic_Prop_Type::make()->categories( $categories )
);
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,181 @@
<?php
namespace Elementor\Modules\AtomicWidgets\DynamicTags;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Modules\AtomicWidgets\Controls\Types\Select_Control;
use Elementor\Modules\AtomicWidgets\Controls\Types\Text_Control;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Transformable_Prop_Type;
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['controls'], $tag['force_convert_to_atomic'] ?? false );
} 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( $controls, $force = false ) {
$atomic_controls = [];
foreach ( $controls as $control ) {
if ( 'section' === $control['type'] ) {
continue;
}
$atomic_control = $this->convert_control_to_atomic( $control );
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 ) {
$map = [
'select' => fn( $control ) => $this->convert_select_control_to_atomic( $control ),
'text' => 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 ) {
if ( empty( $control['options'] ) ) {
throw new \Exception( 'Select control must have options' );
}
$options = array_map(
fn( $key, $value ) => [
'value' => $key,
'label' => $value,
],
array_keys( $control['options'] ),
$control['options']
);
return Select_Control::bind_to( $control['name'] )
->set_label( $control['label'] )
->set_options( $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'] );
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Elementor\Modules\AtomicWidgets\DynamicTags;
use Elementor\Modules\AtomicWidgets\PropsResolver\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, 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,72 @@
<?php
namespace Elementor\Modules\AtomicWidgets\DynamicTags;
use Elementor\Core\DynamicTags\Base_Tag;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_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 = $this->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'];
}
private function convert_control_to_prop_type( array $control ) {
$control_type = $control['type'];
if ( 'text' === $control_type ) {
return String_Prop_Type::make()
->default( $control['default'] ?? null );
}
if ( 'select' === $control_type ) {
return String_Prop_Type::make()
->default( $control['default'] ?? null )
->enum( array_keys( $control['options'] ?? [] ) );
}
return null;
}
}

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\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 Props_Resolver $props_resolver;
public function __construct(
Dynamic_Tags_Manager $dynamic_tags_manager,
Dynamic_Tags_Schemas $dynamic_tags_schemas,
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,15 @@
{% set classes = settings.classes | merge( [ base_styles.base ] ) | join(' ') %}
{% if settings.link.href %}
<a
href="{{ settings.link.href | e('full_url') }}"
target="{{ settings.link.target }}"
class="{{ classes }}"
>
{{ settings.text }}
</a>
{% else %}
<button class="{{ classes }}">
{{ settings.text }}
</button>
{% endif %}

View File

@@ -0,0 +1,129 @@
<?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\Classes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Color_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Link_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Dimensions_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Size_Prop_Type;
use Elementor\Modules\AtomicWidgets\Styles\Style_Definition;
use Elementor\Modules\AtomicWidgets\Styles\Style_Variant;
use Elementor\Modules\WpRest\Classes\WP_Post;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Atomic_Button extends Atomic_Widget_Base {
use Has_Template;
public static function get_element_type(): string {
return 'a-button';
}
public function get_title() {
return esc_html__( 'Atomic Button', 'elementor' );
}
public function get_icon() {
return 'eicon-e-button';
}
protected static function define_props_schema(): array {
return [
'classes' => Classes_Prop_Type::make()
->default( [] ),
'text' => String_Prop_Type::make()
->default( __( 'Click here', 'elementor' ) ),
'link' => Link_Prop_Type::make(),
];
}
protected function define_atomic_controls(): array {
return [
Section::make()
->set_label( __( 'Content', 'elementor' ) )
->set_items( [
Text_Control::bind_to( 'text' )
->set_label( __( 'Button text', 'elementor' ) )
->set_placeholder( __( 'Type your button text here', 'elementor' ) ),
Link_Control::bind_to( 'link' ),
] ),
];
}
protected function define_base_styles(): array {
$color_value = Color_Prop_Type::generate( 'white' );
$font_family_value = String_Prop_Type::generate( 'Poppins' );
$font_size_value = Size_Prop_Type::generate( [
'size' => 16,
'unit' => 'px',
] );
$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( [
'top' => Size_Prop_Type::generate( [
'size' => 12,
'unit' => 'px',
]),
'right' => Size_Prop_Type::generate( [
'size' => 24,
'unit' => 'px',
]),
'bottom' => Size_Prop_Type::generate( [
'size' => 12,
'unit' => 'px',
]),
'left' => 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',
] );
$text_align_value = String_Prop_Type::generate( 'center' );
$font_weight_value = String_Prop_Type::generate( '500' );
return [
'base' => Style_Definition::make()
->add_variant(
Style_Variant::make()
->add_prop( 'color', $color_value )
->add_prop( 'font-family', $font_family_value )
->add_prop( 'font-size', $font_size_value )
->add_prop( 'background', $background_color_value )
->add_prop( 'display', $display_value )
->add_prop( 'font-weight', $font_weight_value )
->add_prop( 'padding', $padding_value )
->add_prop( 'text-align', $text_align_value )
->add_prop( 'border-radius', $border_radius_value )
->add_prop( 'border-width', $border_width_value )
),
];
}
protected function get_templates(): array {
return [
'elementor/elements/atomic-button' => __DIR__ . '/atomic-button.html.twig',
];
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements;
use Elementor\Element_Base;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Prop_Type;
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 = [];
public function __construct( $data = [], $args = null ) {
parent::__construct( $data, $args );
$this->version = $data['version'] ?? '0.0';
$this->styles = $data['styles'] ?? [];
}
abstract protected function define_atomic_controls(): array;
final public function get_initial_config() {
$config = parent::get_initial_config();
$config['atomic_controls'] = $this->get_atomic_controls();
$config['atomic_props_schema'] = static::get_props_schema();
$config['base_styles'] = $this->get_base_styles();
$config['version'] = $this->version;
$config['show_in_panel'] = true;
$config['categories'] = [ 'v4-elements' ];
$config['hide_on_search'] = false;
$config['controls'] = [];
return $config;
}
/**
* @return array<string, Prop_Type>
*/
abstract protected static function define_props_schema(): array;
}

View File

@@ -0,0 +1,11 @@
<{{ settings.tag | e('html_tag') }}
class="{{ settings.classes | merge( [ base_styles.base ] ) | join(' ') }}"
>
{% if settings.link.href %}
<a href="{{ settings.link.href | e('full_url') }}" target="{{ settings.link.target }}">
{{ settings.title }}
</a>
{% else %}
{{ settings.title }}
{% endif %}
</{{ settings.tag | e('html_tag') }}>

View File

@@ -0,0 +1,123 @@
<?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\Color_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\WpRest\Classes\WP_Post;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Atomic_Heading extends Atomic_Widget_Base {
use Has_Template;
public static function get_element_type(): string {
return 'a-heading';
}
public function get_title() {
return esc_html__( 'Atomic Heading', 'elementor' );
}
public function get_icon() {
return 'eicon-e-heading';
}
protected static function define_props_schema(): array {
return [
'classes' => Classes_Prop_Type::make()
->default( [] ),
'tag' => String_Prop_Type::make()
->enum( [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ] )
->default( 'h2' ),
'title' => String_Prop_Type::make()
->default( __( 'Your Title Here', 'elementor' ) ),
'link' => Link_Prop_Type::make(),
];
}
protected function define_atomic_controls(): array {
return [
Section::make()
->set_label( __( 'Content', 'elementor' ) )
->set_items( [
Textarea_Control::bind_to( 'title' )
->set_label( __( 'Title', 'elementor' ) )
->set_placeholder( __( 'Type your title here', 'elementor' ) ),
Select_Control::bind_to( 'tag' )
->set_label( esc_html__( 'Tag', 'elementor' ) )
->set_options( [
[
'value' => 'h1',
'label' => 'H1',
],
[
'value' => 'h2',
'label' => 'H2',
],
[
'value' => 'h3',
'label' => 'H3',
],
[
'value' => 'h4',
'label' => 'H4',
],
[
'value' => 'h5',
'label' => 'H5',
],
[
'value' => 'h6',
'label' => 'H6',
],
]),
Link_Control::bind_to( 'link' ),
] ),
];
}
protected function define_base_styles(): array {
$color_value = Color_Prop_Type::generate( 'black' );
$font_family_value = String_Prop_Type::generate( 'Inter' );
$font_size_value = Size_Prop_Type::generate( [
'size' => 3,
'unit' => 'rem',
] );
$line_height_value = String_Prop_Type::generate( '1.1' );
$font_weight_value = String_Prop_Type::generate( '600' );
return [
'base' => Style_Definition::make()
->add_variant(
Style_Variant::make()
->add_prop( 'color', $color_value )
->add_prop( 'font-family', $font_family_value )
->add_prop( 'font-size', $font_size_value )
->add_prop( 'line-height', $line_height_value )
->add_prop( 'font-weight', $font_weight_value )
),
];
}
protected function get_templates(): array {
return [
'elementor/elements/atomic-heading' => __DIR__ . '/atomic-heading.html.twig',
];
}
}

View File

@@ -0,0 +1,15 @@
{% if settings.link.href %}
<a href="{{ settings.link.href | e('full_url') }}" target="{{ settings.link.target }}">
{% endif %}
<img class="{{ settings.classes | join(' ') }}"
{% 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 %}

View File

@@ -0,0 +1,66 @@
<?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\Controls\Types\Image_Control;
use Elementor\Modules\AtomicWidgets\Image\Placeholder_Image;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Atomic_Image extends Atomic_Widget_Base {
use Has_Template;
public static function get_element_type(): string {
return 'a-image';
}
public function get_title() {
return esc_html__( 'Atomic Image', 'elementor' );
}
public function get_icon() {
return 'eicon-e-image';
}
protected static function define_props_schema(): array {
return [
'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(),
];
}
protected function define_atomic_controls(): array {
$content_section = Section::make()
->set_label( esc_html__( 'Content', 'elementor' ) )
->set_items( [
Image_Control::bind_to( 'image' ),
Link_Control::bind_to( 'link' )
->set_placeholder( __( 'Paste URL or type', 'elementor' ) ),
] );
return [
$content_section,
];
}
protected function get_templates(): array {
return [
'elementor/elements/atomic-image' => __DIR__ . '/atomic-image.html.twig',
];
}
}

View File

@@ -0,0 +1,9 @@
<p class="{{ settings.classes | merge( [ base_styles.base ] ) | join(' ') }}">
{% if settings.link.href %}
<a href="{{ settings.link.href | e('full_url') }}" target="{{ settings.link.target }}">
{{ settings.paragraph }}
</a>
{% else %}
{{ settings.paragraph }}
{% endif %}
</p>

View File

@@ -0,0 +1,90 @@
<?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\Color_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\WpRest\Classes\WP_Post;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Atomic_Paragraph extends Atomic_Widget_Base {
use Has_Template;
public static function get_element_type(): string {
return 'a-paragraph';
}
public function get_title() {
return esc_html__( 'Atomic Paragraph', 'elementor' );
}
public function get_icon() {
return 'eicon-paragraph';
}
protected static function define_props_schema(): array {
return [
'classes' => Classes_Prop_Type::make()
->default( [] ),
'paragraph' => String_Prop_Type::make()
->default( __( 'Type your paragraph here', 'elementor' ) ),
'link' => Link_Prop_Type::make(),
];
}
protected function define_atomic_controls(): array {
return [
Section::make()
->set_label( __( 'Content', 'elementor' ) )
->set_items( [
Textarea_Control::bind_to( 'paragraph' )
->set_label( __( 'Paragraph', 'elementor' ) )
->set_placeholder( __( 'Type your paragraph here', 'elementor' ) ),
Link_Control::bind_to( 'link' ),
] ),
];
}
protected function define_base_styles(): array {
$color_value = Color_Prop_Type::generate( 'black' );
$font_family_value = String_Prop_Type::generate( 'Poppins' );
$font_size_value = Size_Prop_Type::generate( [
'size' => 1.2,
'unit' => 'rem',
] );
$line_height_value = String_Prop_Type::generate( '1.5' );
return [
'base' => Style_Definition::make()
->add_variant(
Style_Variant::make()
->add_prop( 'color', $color_value )
->add_prop( 'font-family', $font_family_value )
->add_prop( 'font-size', $font_size_value )
->add_prop( 'line-height', $line_height_value )
),
];
}
protected function get_templates(): array {
return [
'elementor/elements/atomic-paragraph' => __DIR__ . '/atomic-paragraph.html.twig',
];
}
}

View File

@@ -0,0 +1,115 @@
<?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\Link_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; // Exit if accessed directly.
}
class Atomic_Svg extends Atomic_Widget_Base {
const BASE_STYLE_KEY = 'base';
const DEFAULT_SIZE = 'full';
const DEFAULT_SVG_PATH = ELEMENTOR_ASSETS_URL . 'images/a-default-svg.svg';
public static function get_element_type(): string {
return 'a-svg';
}
public function get_title() {
return esc_html__( 'Atomic SVG', 'elementor' );
}
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( self::DEFAULT_SVG_PATH ),
'link' => Link_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' ),
Link_Control::bind_to( 'link' )
->set_placeholder( __( 'Paste URL or type', 'elementor' ) ),
] ),
];
}
protected function define_base_styles(): array {
$width = Size_Prop_Type::generate( [
'size' => 65,
'unit' => 'px',
] );
$height = Size_Prop_Type::generate( [
'size' => 65,
'unit' => 'px',
] );
return [
self::BASE_STYLE_KEY => Style_Definition::make()
->add_variant(
Style_Variant::make()
->add_prop( 'width', $width )
->add_prop( 'height', $height )
),
];
}
protected function render() {
$settings = $this->get_atomic_settings();
$svg_url = isset( $settings['svg']['url'] ) ? $settings['svg']['url'] : null;
if ( ! $svg_url && isset( $settings['svg']['id'] ) ) {
$attachment = wp_get_attachment_image_src( $settings['svg']['id'], self::DEFAULT_SIZE );
$svg_url = isset( $attachment[0] ) ? $attachment[0] : null;
}
$svg = file_get_contents( $svg_url );
$svg = $svg ? new \WP_HTML_Tag_Processor( $svg ) : null;
if ( $svg && $svg->next_tag( 'svg' ) ) {
$this->set_svg_attributes( $svg, $settings );
}
if ( $svg ) {
$svg_html = ( new Svg_Sanitizer() )->sanitize( $svg->get_updated_html() );
}
$svg_html = $svg_html ?? file_get_contents( self::DEFAULT_SVG_PATH );
if ( isset( $settings['link'] ) && ! empty( $settings['link']['href'] ) ) {
$svg_html = sprintf( '<a href="%s" target="%s"> %s </a>', esc_url( $settings['link']['href'] ), esc_attr( $settings['link']['target'] ), $svg_html );
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $svg_html;
}
private function set_svg_attributes( \WP_HTML_Tag_Processor $svg, $settings ) {
$svg->set_attribute( 'fill', 'currentColor' );
$string_classes = implode( ' ', $settings['classes'] );
$svg->add_class( $string_classes );
$base_styles = $this->get_base_styles_dictionary()[ self::BASE_STYLE_KEY ];
$svg->add_class( $base_styles );
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements;
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 = [];
public function __construct( $data = [], $args = null ) {
parent::__construct( $data, $args );
$this->version = $data['version'] ?? '0.0';
$this->styles = $data['styles'] ?? [];
}
abstract protected function define_atomic_controls(): array;
final public function get_initial_config() {
$config = parent::get_initial_config();
$config['atomic'] = true;
$config['atomic_controls'] = $this->get_atomic_controls();
$config['base_styles'] = $this->get_base_styles();
$config['atomic_props_schema'] = static::get_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;
}

View File

@@ -0,0 +1,151 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements\Div_Block;
use Elementor\Modules\AtomicWidgets\Controls\Types\Link_Control;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Element_Base;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Modules\AtomicWidgets\Controls\Types\Select_Control;
use Elementor\Modules\AtomicWidgets\PropTypes\Classes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Link_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
use Elementor\Plugin;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Div_Block extends Atomic_Element_Base {
public static function get_type() {
return 'div-block';
}
public static function get_element_type(): string {
return 'div-block';
}
public function get_title() {
return esc_html__( 'Div Block', 'elementor' );
}
public function get_icon() {
return 'eicon-div-block';
}
protected static function define_props_schema(): array {
return [
'classes' => Classes_Prop_Type::make()
->default( [] ),
'tag' => String_Prop_Type::make()
->enum( [ 'div', 'header', 'section', 'article', 'aside', 'footer' ] )
->default( 'div' ),
'link' => Link_Prop_Type::make(),
];
}
protected function define_atomic_controls(): array {
return [
Section::make()
->set_label( __( 'Settings', 'elementor' ) )
->set_items( [
Select_Control::bind_to( 'tag' )
->set_label( esc_html__( 'HTML Tag', 'elementor' ) )
->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',
],
]),
Link_Control::bind_to( 'link' )
->set_placeholder( __( 'Paste URL or type', 'elementor' ) ),
]),
];
}
public function get_style_depends() {
return [ 'div-block' ];
}
protected function _get_default_child_type( array $element_data ) {
if ( 'div-block' === $element_data['elType'] ) {
return Plugin::$instance->elements_manager->get_element_types( 'div-block' );
}
return Plugin::$instance->widgets_manager->get_widget_types( $element_data['widgetType'] );
}
protected function content_template() {
?>
<?php
}
protected function add_render_attributes() {
parent::add_render_attributes();
$settings = $this->get_atomic_settings();
$attributes = [
'class' => [
'e-con',
'e-div-block',
...( $settings['classes'] ?? [] ),
],
];
if ( ! empty( $settings['link']['href'] ) ) {
$attributes = array_merge( $attributes, $settings['link'] );
}
$this->add_render_attribute( '_wrapper', $attributes );
}
public function before_render() {
?>
<<?php $this->print_html_tag(); ?> <?php $this->print_render_attribute_string( '_wrapper' ); ?>>
<?php
}
public function after_render() {
?>
</<?php $this->print_html_tag(); ?>>
<?php
}
/**
* 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 );
}
protected function get_html_tag(): string {
$settings = $this->get_atomic_settings();
return ! empty( $settings['link']['href'] ) ? 'a' : ( $settings['tag'] ?? 'div' );
}
}

View File

@@ -0,0 +1,168 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Elements;
use Elementor\Element_Base;
use Elementor\Modules\AtomicWidgets\Base\Atomic_Control_Base;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Modules\AtomicWidgets\PropsResolver\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\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 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 ) {
[ $is_valid, $sanitized_style, $errors ] = $style_parser->parse( $style );
if ( ! $is_valid ) {
throw new \Exception( esc_html( 'Styles validation failed. Invalid keys: ' . join( ', ', $errors ) ) );
}
$styles[ $style_id ] = $sanitized_style;
}
return $styles;
}
private function parse_atomic_settings( array $settings ): array {
$schema = static::get_props_schema();
$props_parser = Props_Parser::make( $schema );
[ $is_valid, $parsed, $errors ] = $props_parser->parse( $settings );
if ( ! $is_valid ) {
throw new \Exception( esc_html( 'Settings validation failed. Invalid keys: ' . join( ', ', $errors ) ) );
}
return $parsed;
}
public function get_atomic_controls() {
$controls = $this->define_atomic_controls();
$schema = static::get_props_schema();
// Validate the schema only in the Editor.
static::validate_schema( $schema );
return $this->get_valid_controls( $schema, $controls );
}
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'] );
return $data;
}
final public function get_raw_data( $with_html_content = false ) {
$raw_data = parent::get_raw_data( $with_html_content );
$raw_data['styles'] = $this->styles;
return $raw_data;
}
final public function get_stack( $with_common_controls = true ) {
return [
'controls' => [],
'tabs' => [],
];
}
public function get_atomic_settings(): array {
$schema = static::get_props_schema();
$props = $this->get_settings();
return Props_Resolver::for_settings()->resolve( $schema, $props );
}
public static function get_props_schema(): array {
return apply_filters(
'elementor/atomic-widgets/props-schema',
static::define_props_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,62 @@
<?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 {
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_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,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.png';
}
public static function get_background_placeholder_image() {
return ELEMENTOR_ASSETS_URL . 'images/background-placeholder.png';
}
}

View File

@@ -0,0 +1,210 @@
<?php
namespace Elementor\Modules\AtomicWidgets;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Experiments\Manager as Experiments_Manager;
use Elementor\Elements_Manager;
use Elementor\Modules\AtomicWidgets\DynamicTags\Dynamic_Tags_Module;
use Elementor\Modules\AtomicWidgets\Elements\Div_Block\Div_Block;
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_Svg\Atomic_Svg;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Array_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Combine_Array_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Settings\Image_Src_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Settings\Image_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Settings\Link_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Primitive_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Background_Color_Overlay_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Background_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Edge_Sizes_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Corner_Sizes_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Dimensions_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Styles\Layout_Direction_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_Image_Position_Offset_Transformer;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformers_Registry;
use Elementor\Modules\AtomicWidgets\PropTypes\Background_Color_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\Layout_Direction_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Link_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\Boolean_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Classes_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Color_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Image_Attachment_Id_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\Primitives\Number_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Shadow_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Size_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Stroke_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Url_Prop_Type;
use Elementor\Modules\AtomicWidgets\Styles\Atomic_Widget_Base_Styles;
use Elementor\Modules\AtomicWidgets\Styles\Atomic_Widget_Styles;
use Elementor\Modules\AtomicWidgets\Styles\Style_Schema;
use Elementor\Plugin;
use Elementor\Widgets_Manager;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseModule {
const EXPERIMENT_NAME = 'atomic_widgets';
const PACKAGES = [
'editor-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-panels',
'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 ( 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();
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/widgets/register', fn( Widgets_Manager $widgets_manager ) => $this->register_widgets( $widgets_manager ) );
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/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_styles', fn() => $this->register_styles() );
}
}
public static function get_experimental_data(): array {
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 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 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() );
}
private function register_elements( Elements_Manager $elements_manager ) {
$elements_manager->register_element_type( new Div_Block() );
}
private function register_settings_transformers( Transformers_Registry $transformers ) {
// Primitives
$transformers->register( Boolean_Prop_Type::get_key(), new Primitive_Transformer() );
$transformers->register( Number_Prop_Type::get_key(), new Primitive_Transformer() );
$transformers->register( String_Prop_Type::get_key(), new Primitive_Transformer() );
// Other
$transformers->register( Classes_Prop_Type::get_key(), new Array_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( Image_Attachment_Id_Prop_Type::get_key(), new Primitive_Transformer() );
$transformers->register( Url_Prop_Type::get_key(), new Primitive_Transformer() );
$transformers->register( Link_Prop_Type::get_key(), new Link_Transformer() );
}
private function register_styles_transformers( Transformers_Registry $transformers ) {
// Primitives
$transformers->register( Boolean_Prop_Type::get_key(), new Primitive_Transformer() );
$transformers->register( Number_Prop_Type::get_key(), new Primitive_Transformer() );
$transformers->register( String_Prop_Type::get_key(), new Primitive_Transformer() );
// Other
$transformers->register( Dimensions_Prop_Type::get_key(), new Dimensions_Transformer() );
$transformers->register( Size_Prop_Type::get_key(), new Size_Transformer() );
$transformers->register( Color_Prop_Type::get_key(), new Primitive_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( Border_Radius_Prop_Type::get_key(), new Corner_Sizes_Transformer( fn( $corner ) => 'border-' . $corner . '-radius' ) );
$transformers->register( Border_Width_Prop_Type::get_key(), new Edge_Sizes_Transformer( fn( $edge ) => 'border-' . $edge . '-width' ) );
$transformers->register( Stroke_Prop_Type::get_key(), new Stroke_Transformer() );
$transformers->register( Layout_Direction_Prop_Type::get_key(), new Layout_Direction_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( Image_Attachment_Id_Prop_Type::get_key(), new Primitive_Transformer() );
$transformers->register( Url_Prop_Type::get_key(), new Primitive_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 Background_Image_Position_Offset_Transformer() );
$transformers->register( Background_Color_Overlay_Prop_Type::get_key(), new Background_Color_Overlay_Transformer() );
$transformers->register( Background_Overlay_Prop_Type::get_key(), new Combine_Array_Transformer( ',' ) );
$transformers->register( Background_Prop_Type::get_key(), new Background_Transformer() );
}
/**
* 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
);
}
public function register_styles() {
wp_register_style(
'div-block',
$this->get_css_assets_url( 'div-block', 'assets/css/' ),
[ 'elementor-frontend' ],
ELEMENTOR_VERSION
);
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Parsers;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Props_Parser {
private array $schema;
private array $errors_bag = [];
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
*
* @return array{
* 0: bool,
* 1: array<string, mixed>,
* 2: array<string>
* }
*/
public function validate( array $props ): array {
$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 ) {
$this->errors_bag[] = $key;
continue;
}
if ( ! is_null( $value ) ) {
$validated[ $key ] = $value;
}
}
$is_valid = empty( $this->errors_bag );
return [
$is_valid,
$validated,
$this->errors_bag,
];
}
/**
* @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
*
* @return array<string, mixed>
*/
public function sanitize( array $props ): array {
$sanitized = [];
foreach ( $this->schema as $key => $prop_type ) {
if ( ! isset( $props[ $key ] ) ) {
continue;
}
$sanitized[ $key ] = $prop_type->sanitize( $props[ $key ] );
}
return $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
*
* @return array{
* 0: bool,
* 1: array<string, mixed>,
* 2: array<string>
* }
*/
public function parse( array $props ): array {
[ $is_valid, $validated, $errors_bag ] = $this->validate( $props );
return [
$is_valid,
$this->sanitize( $validated ),
$errors_bag,
];
}
}

View File

@@ -0,0 +1,161 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Parsers;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Style_Parser {
const VALID_TYPES = [
'class',
];
const VALID_STATES = [
'hover',
'active',
'focus',
null,
];
private array $schema;
private array $errors_bag = [];
private $should_validate_id = true;
public function __construct( array $schema ) {
$this->schema = $schema;
}
public static function make( array $schema ): self {
return new static( $schema );
}
public function without_id() {
$this->should_validate_id = false;
return $this;
}
/**
* @param array $style
* the style object to validate
*
* @return array{
* 0: bool,
* 1: array<string, mixed>,
* 2: array<string>
* }
*/
public function validate( array $style ): array {
$validated_style = $style;
if ( $this->should_validate_id && ( ! isset( $style['id'] ) || ! is_string( $style['id'] ) ) ) {
$this->errors_bag[] = 'id';
}
if ( ! isset( $style['type'] ) || ! in_array( $style['type'], self::VALID_TYPES, true ) ) {
$this->errors_bag[] = 'type';
}
if ( ! isset( $style['label'] ) || ! is_string( $style['label'] ) ) {
$this->errors_bag[] = 'label';
}
if ( ! isset( $style['variants'] ) || ! is_array( $style['variants'] ) ) {
$this->errors_bag[] = 'variants';
unset( $validated_style['variants'] );
return [
false,
$validated_style,
$this->errors_bag,
];
}
$props_parser = Props_Parser::make( $this->schema );
foreach ( $style['variants'] as $variant_index => $variant ) {
if ( ! isset( $variant['meta'] ) ) {
$this->errors_bag[] = 'meta';
continue;
}
$is_variant_meta_valid = $this->validate_meta( $variant['meta'] );
if ( $is_variant_meta_valid ) {
[, $validated_props, $variant_errors] = $props_parser->validate( $variant['props'] );
$this->errors_bag = array_merge( $this->errors_bag, $variant_errors );
$validated_style['variants'][ $variant_index ]['props'] = $validated_props;
} else {
unset( $validated_style['variants'][ $variant_index ] );
}
}
$is_valid = empty( $this->errors_bag );
return [
$is_valid,
$validated_style,
$this->errors_bag,
];
}
public function validate_meta( $meta ): bool {
if ( ! is_array( $meta ) ) {
$this->errors_bag[] = 'meta';
return false;
}
if ( ! array_key_exists( 'state', $meta ) || ! in_array( $meta['state'], self::VALID_STATES, true ) ) {
$this->errors_bag[] = 'meta';
return false;
}
// TODO: Validate breakpoint based on the existing breakpoints in the system [EDS-528]
if ( ! isset( $meta['breakpoint'] ) || ! is_string( $meta['breakpoint'] ) ) {
$this->errors_bag[] = 'meta';
return false;
}
return true;
}
/**
* @param array $style
* the style object to sanitize
*
* @return array<string, mixed>
*/
public function sanitize( array $style ): array {
$props_parser = Props_Parser::make( $this->schema );
if ( ! empty( $style['variants'] ) ) {
foreach ( $style['variants'] as $variant_index => $variant ) {
$style['variants'][ $variant_index ]['props'] = $props_parser->sanitize( $variant['props'] );
}
}
return $style;
}
/**
* @param array $style
* the style object to parse
*
* @return array{
* 0: bool,
* 1: array<string, mixed>,
* 2: array<string>
* }
*/
public function parse( array $style ): array {
[ $is_valid, $validated, $errors_bag ] = $this->validate( $style );
return [
$is_valid,
$this->sanitize( $validated ),
$errors_bag,
];
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes;
class Background_Color_Overlay_Prop_Type extends Color_Prop_Type {
public static function get_key(): string {
return 'background-color-overlay';
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Object_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
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( self::get_position_enum_values() ) )
->add_prop_type( Background_Image_Position_Offset_Prop_Type::make() ),
'attachment' => String_Prop_Type::make()->enum( [ 'fixed', 'scroll' ] ),
];
}
private static function get_position_enum_values(): array {
return [
'center center',
'center left',
'center right',
'top center',
'top left',
'top right',
'bottom center',
'bottom left',
'bottom right',
];
}
}

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(),
];
}
}

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_Image_Position_Offset_Prop_Type extends Object_Prop_Type {
public static function get_key(): string {
return 'background-image-position-offset';
}
protected function define_shape(): array {
return [
'x' => Size_Prop_Type::make(),
'y' => Size_Prop_Type::make(),
];
}
}

View File

@@ -0,0 +1,22 @@
<?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 Background_Overlay_Prop_Type extends Array_Prop_Type {
public static function get_key(): string {
return 'background-overlay';
}
protected function define_item_type(): Prop_Type {
return Union_Prop_Type::make()
->add_prop_type( Background_Color_Overlay_Prop_Type::make() )
->add_prop_type( Background_Image_Overlay_Prop_Type::make() );
}
}

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_Prop_Type extends Object_Prop_Type {
public static function get_key(): string {
return 'background';
}
protected function define_shape(): array {
return [
'background-overlay' => Background_Overlay_Prop_Type::make(),
'color' => Color_Prop_Type::make(),
];
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes\Base;
use Elementor\Modules\AtomicWidgets\PropTypes\Concerns;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Transformable_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Array_Prop_Type implements Transformable_Prop_Type {
const KIND = 'array';
use Concerns\Has_Default;
use Concerns\Has_Generate;
use Concerns\Has_Meta;
use Concerns\Has_Required_Setting;
use Concerns\Has_Settings;
use Concerns\Has_Transformable_Validation;
protected Prop_Type $item_type;
public function __construct() {
$this->item_type = $this->define_item_type();
}
/**
* @return static
*/
public static function make() {
return new static();
}
/**
* @param Prop_Type $item_type
*
* @return $this
*/
public function set_item_type( Prop_Type $item_type ) {
$this->item_type = $item_type;
return $this;
}
public function get_item_type(): Prop_Type {
return $this->item_type;
}
public function validate( $value ): bool {
if ( is_null( $value ) ) {
return ! $this->is_required();
}
return (
$this->is_transformable( $value ) &&
$this->validate_value( $value['value'] )
);
}
protected function validate_value( $value ): bool {
if ( ! is_array( $value ) ) {
return false;
}
$prop_type = $this->get_item_type();
foreach ( $value as $item ) {
if ( $prop_type && ! $prop_type->validate( $item ) ) {
return false;
}
}
return true;
}
public function sanitize( $value ) {
$value['value'] = $this->sanitize_value( $value['value'] );
return $value;
}
public function sanitize_value( $value ) {
$prop_type = $this->get_item_type();
return array_map( function ( $item ) use ( $prop_type ) {
return $prop_type->sanitize( $item );
}, $value );
}
public function jsonSerialize(): array {
return [
'kind' => static::KIND,
'key' => static::get_key(),
'default' => $this->get_default(),
'meta' => (object) $this->get_meta(),
'settings' => (object) $this->get_settings(),
'item_prop_type' => $this->get_item_type(),
];
}
abstract protected function define_item_type(): Prop_Type;
}

View File

@@ -0,0 +1,141 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes\Base;
use Elementor\Modules\AtomicWidgets\PropTypes\Concerns;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Transformable_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Prop_Type;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Object_Prop_Type implements Transformable_Prop_Type {
const KIND = 'object';
use Concerns\Has_Default;
use Concerns\Has_Generate;
use Concerns\Has_Meta;
use Concerns\Has_Required_Setting;
use Concerns\Has_Settings;
use Concerns\Has_Transformable_Validation;
/**
* @var array<Prop_Type>
*/
protected array $shape;
public function __construct() {
$this->shape = $this->define_shape();
}
public function get_default() {
if ( null !== $this->default ) {
return $this->default;
}
foreach ( $this->get_shape() as $item ) {
// If the object has at least one property with default, return an empty object so
// it'll be iterable for processes like validation / transformation.
if ( $item->get_default() !== null ) {
return static::generate( [] );
}
}
return null;
}
/**
* @return static
*/
public static function make() {
return new static();
}
/**
* @param array $shape
*
* @return $this
*/
public function set_shape( array $shape ) {
$this->shape = $shape;
return $this;
}
public function get_shape(): array {
return $this->shape;
}
public function get_shape_field( $key ): ?Prop_Type {
return $this->shape[ $key ] ?? null;
}
public function validate( $value ): bool {
if ( is_null( $value ) ) {
return ! $this->is_required();
}
return (
$this->is_transformable( $value ) &&
$this->validate_value( $value['value'] )
);
}
protected function validate_value( $value ): bool {
if ( ! is_array( $value ) ) {
return false;
}
foreach ( $this->get_shape() as $key => $prop_type ) {
if ( ! ( $prop_type instanceof Prop_Type ) ) {
Utils::safe_throw( "Object prop type must have a prop type for key: $key" );
}
if ( ! $prop_type->validate( $value[ $key ] ?? $prop_type->get_default() ) ) {
return false;
}
}
return true;
}
public function sanitize( $value ) {
$value['value'] = $this->sanitize_value( $value['value'] );
return $value;
}
public function sanitize_value( $value ) {
foreach ( $this->get_shape() as $key => $prop_type ) {
if ( ! isset( $value[ $key ] ) ) {
continue;
}
$sanitized_value = $prop_type->sanitize( $value[ $key ] );
$value[ $key ] = $sanitized_value;
}
return $value;
}
public function jsonSerialize(): array {
$default = $this->get_default();
return [
'kind' => static::KIND,
'key' => static::get_key(),
'default' => is_array( $default ) ? (object) $default : $default,
'meta' => (object) $this->get_meta(),
'settings' => (object) $this->get_settings(),
'shape' => (object) $this->get_shape(),
];
}
/**
* @return array<Prop_Type>
*/
abstract protected function define_shape(): array;
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes\Base;
use Elementor\Modules\AtomicWidgets\PropTypes\Concerns;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Transformable_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Plain_Prop_Type implements Transformable_Prop_Type {
const KIND = 'plain';
use Concerns\Has_Default;
use Concerns\Has_Generate;
use Concerns\Has_Meta;
use Concerns\Has_Required_Setting;
use Concerns\Has_Settings;
use Concerns\Has_Transformable_Validation;
/**
* @return static
*/
public static function make() {
return new static();
}
public function validate( $value ): bool {
if ( is_null( $value ) ) {
return ! $this->is_required();
}
return (
$this->is_transformable( $value ) &&
$this->validate_value( $value['value'] )
);
}
public function sanitize( $value ) {
$value['value'] = $this->sanitize_value( $value['value'] );
return $value;
}
public function jsonSerialize(): array {
return [
'kind' => static::KIND,
'key' => static::get_key(),
'default' => $this->get_default(),
'meta' => (object) $this->get_meta(),
'settings' => (object) $this->get_settings(),
];
}
abstract public static function get_key(): string;
abstract protected function validate_value( $value ): bool;
abstract protected function sanitize_value( $value );
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Object_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Border_Radius_Prop_Type extends Object_Prop_Type {
public static function get_key(): string {
return 'border-radius';
}
protected function define_shape(): array {
return [
'top-left' => Size_Prop_Type::make(),
'top-right' => Size_Prop_Type::make(),
'bottom-right' => Size_Prop_Type::make(),
'bottom-left' => Size_Prop_Type::make(),
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Object_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Border_Width_Prop_Type extends Object_Prop_Type {
public static function get_key(): string {
return 'border-width';
}
protected function define_shape(): array {
return [
'top' => Size_Prop_Type::make(),
'right' => Size_Prop_Type::make(),
'bottom' => Size_Prop_Type::make(),
'left' => Size_Prop_Type::make(),
];
}
}

View File

@@ -0,0 +1,20 @@
<?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 Box_Shadow_Prop_Type extends Array_Prop_Type {
public static function get_key(): string {
return 'box-shadow';
}
protected function define_item_type(): Prop_Type {
return Shadow_Prop_Type::make();
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Plain_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Classes_Prop_Type extends Plain_Prop_Type {
public static function get_key(): string {
return 'classes';
}
protected function validate_value( $value ): bool {
if ( ! is_array( $value ) ) {
return false;
}
foreach ( $value as $class_name ) {
if ( ! is_string( $class_name ) || ! preg_match( '/^[a-z][a-z-_0-9]*$/i', $class_name ) ) {
return false;
}
}
return true;
}
protected function sanitize_value( $value ) {
if ( ! is_array( $value ) ) {
return null;
}
$sanitized = array_map(function ( $class_name ) {
if ( ! is_string( $class_name ) ) {
return null;
}
return sanitize_text_field( $class_name );
}, $value);
return array_filter($sanitized, function ( $class_name ) {
return ! empty( $class_name );
});
}
}

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 Color_Gradient_Prop_Type extends Object_Prop_Type {
public static function get_key(): string {
return 'color-gradient';
}
protected function define_shape(): array {
return [
'color' => Color_Prop_Type::make()->required(),
];
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Color_Prop_Type extends String_Prop_Type {
public static function get_key(): string {
return 'color';
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes\Concerns;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Transformable_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
trait Has_Default {
protected $default = null;
/**
* @param $value
*
* @return $this
*/
public function default( $value ) {
$this->default = static::generate( $value );
return $this;
}
public function get_default() {
return $this->default;
}
abstract public static function generate( $value );
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes\Concerns;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Transformable_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
trait Has_Generate {
public static function generate( $value ) {
return [
'$$type' => static::get_key(),
'value' => $value,
];
}
abstract public static function get_key(): string;
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes\Concerns;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
trait Has_Meta {
protected array $meta = [];
/**
* @param $key
* @param $value
*
* @return $this
*/
public function meta( $key, $value = null ) {
$is_tuple = is_array( $key ) && 2 === count( $key );
if ( $is_tuple ) {
[ $key, $value ] = $key;
}
$this->meta[ $key ] = $value;
return $this;
}
public function get_meta(): array {
return $this->meta;
}
public function get_meta_item( $key, $default = null ) {
return array_key_exists( $key, $this->meta ) ? $this->meta[ $key ] : $default;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes\Concerns;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
trait Has_Required_Setting {
protected function is_required(): bool {
return $this->get_setting( 'required', false );
}
public function required() {
$this->setting( 'required', true );
return $this;
}
public function optional() {
$this->setting( 'required', false );
return $this;
}
abstract public function get_setting( string $key, $default = null );
abstract public function setting( $key, $value );
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes\Concerns;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
trait Has_Settings {
protected array $settings = [];
/**
* @param $key
* @param $value
*
* @return $this
*/
public function setting( $key, $value ) {
$this->settings[ $key ] = $value;
return $this;
}
public function get_settings(): array {
return $this->settings;
}
public function get_setting( string $key, $default = null ) {
return array_key_exists( $key, $this->settings ) ? $this->settings[ $key ] : $default;
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes\Concerns;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
trait Has_Transformable_Validation {
protected function is_transformable( $value ): bool {
$satisfies_basic_shape = (
is_array( $value ) &&
array_key_exists( '$$type', $value ) &&
array_key_exists( 'value', $value ) &&
static::get_key() === $value['$$type']
);
$supports_disabling = (
! isset( $value['disabled'] ) ||
is_bool( $value['disabled'] )
);
return (
$satisfies_basic_shape &&
$supports_disabling
);
}
abstract public static function get_key(): string;
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes\Contracts;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
interface Prop_Type extends \JsonSerializable {
public function get_default();
public function validate( $value ): bool;
public function sanitize( $value );
public function get_meta(): array;
public function get_meta_item( string $key, $default = null );
public function get_settings(): array;
public function get_setting( string $key, $default = null );
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes\Contracts;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
interface Transformable_Prop_Type extends Prop_Type {
public static function get_key(): string;
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Object_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\Boolean_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Dimensions_Prop_Type extends Object_Prop_Type {
public static function get_key(): string {
return 'dimensions';
}
protected function define_shape(): array {
return [
'top' => Size_Prop_Type::make(),
'right' => Size_Prop_Type::make(),
'bottom' => Size_Prop_Type::make(),
'left' => Size_Prop_Type::make(),
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Plain_Prop_Type;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Image_Attachment_Id_Prop_Type extends Plain_Prop_Type {
public static function get_key(): string {
return 'image-attachment-id';
}
protected function validate_value( $value ): bool {
return is_numeric( $value ) && Plugin::$instance->wp->wp_attachment_is_image( $value );
}
protected function sanitize_value( $value ): int {
return (int) $value;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes;
use Elementor\Modules\AtomicWidgets\Image\Image_Sizes;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Object_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Image_Prop_Type extends Object_Prop_Type {
public static function get_key(): string {
return 'image';
}
protected function define_shape(): array {
return [
'src' => Image_Src_Prop_Type::make()->required(),
'size' => String_Prop_Type::make()->enum( Image_Sizes::get_keys() )->required(),
];
}
public function default_url( string $url ): self {
$this->get_shape_field( 'src' )->default( [
'id' => null,
'url' => Url_Prop_Type::generate( $url ),
] );
return $this;
}
public function default_size( string $size ): self {
$this->get_shape_field( 'size' )->default( $size );
return $this;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Object_Prop_Type;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Image_Src_Prop_Type extends Object_Prop_Type {
public static function get_key(): string {
return 'image-src';
}
protected function define_shape(): array {
return [
'id' => Image_Attachment_Id_Prop_Type::make(),
'url' => Url_Prop_Type::make(),
];
}
public function default_url( string $url ): self {
$this->default( [
'id' => null,
'url' => Url_Prop_Type::generate( $url ),
] );
return $this;
}
protected function validate_value( $value ): bool {
$only_one_key = count( array_filter( $value ) ) === 1;
return $only_one_key && parent::validate_value( $value );
}
}

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 Layout_Direction_Prop_Type extends Object_Prop_Type {
public static function get_key(): string {
return 'layout-direction';
}
protected function define_shape(): array {
return [
'column' => Size_Prop_Type::make(),
'row' => Size_Prop_Type::make(),
];
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Object_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;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Link_Prop_Type extends Object_Prop_Type {
public static function get_key(): string {
return 'link';
}
protected function define_shape(): array {
return [
'destination' => Union_Prop_Type::make()
->add_prop_type( Url_Prop_Type::make() )
->add_prop_type( Number_Prop_Type::make() )
->required(),
'label' => Union_Prop_Type::make()
->add_prop_type( String_Prop_Type::make() ),
'isTargetBlank' => Boolean_Prop_Type::make(),
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes\Primitives;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Plain_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Boolean_Prop_Type extends Plain_Prop_Type {
public static function get_key(): string {
return 'boolean';
}
protected function validate_value( $value ): bool {
return is_bool( $value );
}
protected function sanitize_value( $value ) {
return (bool) $value;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes\Primitives;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Plain_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Number_Prop_Type extends Plain_Prop_Type {
public static function get_key(): string {
return 'number';
}
protected function validate_value( $value ): bool {
return is_numeric( $value );
}
protected function sanitize_value( $value ) {
return (int) $value;
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes\Primitives;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Plain_Prop_Type;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class String_Prop_Type extends Plain_Prop_Type {
public static function get_key(): string {
return 'string';
}
public function enum( array $allowed_values ): self {
$all_are_strings = array_reduce(
$allowed_values,
fn ( $carry, $item ) => $carry && is_string( $item ),
true
);
if ( ! $all_are_strings ) {
Utils::safe_throw( 'All values in an enum must be strings.' );
}
$this->settings['enum'] = $allowed_values;
return $this;
}
public function get_enum() {
return $this->settings['enum'] ?? null;
}
public function regex( $pattern ) {
if ( ! is_string( $pattern ) ) {
Utils::safe_throw( 'Pattern must be a string, and valid regex pattern' );
}
$this->settings['regex'] = $pattern;
return $this;
}
public function get_regex() {
return $this->settings['regex'] ?? null;
}
protected function validate_value( $value ): bool {
return (
is_string( $value ) &&
( ! $this->get_enum() || $this->validate_enum( $value ) ) &&
( ! $this->get_regex() || $this->validate_regex( $value ) )
);
}
private function validate_enum( $value ): bool {
return in_array( $value, $this->settings['enum'], true );
}
private function validate_regex( $value ): bool {
return preg_match( $this->settings['regex'], $value );
}
protected function sanitize_value( $value ) {
return sanitize_text_field( $value );
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Object_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Shadow_Prop_Type extends Object_Prop_Type {
public static function get_key(): string {
return 'shadow';
}
protected function define_shape(): array {
return [
'hOffset' => Size_Prop_Type::make()->required(),
'vOffset' => Size_Prop_Type::make()->required(),
'blur' => Size_Prop_Type::make()->required(),
'spread' => Size_Prop_Type::make()->required(),
'color' => Color_Prop_Type::make()->required(),
'position' => String_Prop_Type::make()->enum( [ 'inset' ] ),
];
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Plain_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\Number_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Size_Prop_Type extends Plain_Prop_Type {
const SUPPORTED_UNITS = [ 'px', 'em', 'rem', '%', 'vh', 'vw', 'vmin', 'vmax' ];
public static function get_key(): string {
return 'size';
}
protected function validate_value( $value ): bool {
return (
is_array( $value ) &&
array_key_exists( 'size', $value ) &&
is_numeric( $value['size'] ) &&
! empty( $value['unit'] ) &&
in_array( $value['unit'], static::SUPPORTED_UNITS, true )
);
}
protected function sanitize_value( $value ) {
return [
'size' => (int) $value['size'],
'unit' => sanitize_text_field( $value['unit'] ),
];
}
}

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 Stroke_Prop_Type extends Object_Prop_Type {
public static function get_key(): string {
return 'stroke';
}
protected function define_shape(): array {
return [
'color' => Color_Prop_Type::make(),
'width' => Size_Prop_Type::make(),
];
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Transformable_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Union_Prop_Type implements Prop_Type {
const KIND = 'union';
use Concerns\Has_Meta;
use Concerns\Has_Settings;
use Concerns\Has_Required_Setting;
protected $default = null;
/** @var Array<string, Transformable_Prop_Type> */
protected array $prop_types = [];
public static function make(): self {
return new static();
}
public static function create_from( Transformable_Prop_Type $prop_type ): self {
return static::make()
->add_prop_type( $prop_type )
->default( $prop_type->get_default() );
}
public function add_prop_type( Transformable_Prop_Type $prop_type ): self {
$this->prop_types[ $prop_type::get_key() ] = $prop_type;
return $this;
}
public function get_prop_types(): array {
return $this->prop_types;
}
public function get_prop_type( $type ): ?Transformable_Prop_Type {
return $this->prop_types[ $type ] ?? null;
}
private function get_prop_type_from_value( $value ): ?Prop_Type {
if ( isset( $value['$$type'] ) ) {
return $this->get_prop_type( $value['$$type'] );
}
if ( is_numeric( $value ) ) {
return $this->get_prop_type( 'number' );
}
if ( is_bool( $value ) ) {
return $this->get_prop_type( 'boolean' );
}
if ( is_string( $value ) ) {
return $this->get_prop_type( 'string' );
}
return null;
}
public function default( $value, ?string $type = null ): self {
$this->default = ! $type ?
$value :
[
'$$type' => $type,
'value' => $value,
];
return $this;
}
public function get_default() {
return $this->default;
}
public function validate( $value ): bool {
if ( is_null( $value ) ) {
return ! $this->is_required();
}
$prop_type = $this->get_prop_type_from_value( $value );
return $prop_type && $prop_type->validate( $value );
}
public function sanitize( $value ) {
$prop_type = $this->get_prop_type_from_value( $value );
return $prop_type ? $prop_type->sanitize( $value ) : null;
}
public function jsonSerialize(): array {
return [
'kind' => static::KIND,
'default' => $this->get_default(),
'meta' => $this->get_meta(),
'settings' => $this->get_settings(),
'prop_types' => $this->get_prop_types(),
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropTypes;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Plain_Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Url_Prop_Type extends Plain_Prop_Type {
public static function get_key(): string {
return 'url';
}
public static function validate_url( $value ): bool {
return (bool) wp_http_validate_url( $value );
}
protected function validate_value( $value ): bool {
return self::validate_url( $value );
}
protected function sanitize_value( $value ) {
return esc_url_raw( $value );
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropsResolver;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Multi_Props {
public static function is( $value ) {
return (
! empty( $value['$$multi-props'] ) &&
true === $value['$$multi-props'] &&
array_key_exists( 'value', $value )
);
}
public static function generate( $value ) {
return [
'$$multi-props' => true,
'value' => $value,
];
}
public static function get_value( $value ) {
return $value['value'] ?? null;
}
}

View File

@@ -0,0 +1,171 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropsResolver;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Array_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Object_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Union_Prop_Type;
use Exception;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Props_Resolver {
/**
* Each transformer can return a value that is also a transformable value,
* which means that it can be transformed again by another transformer.
* This constant defines the maximum depth of transformations to avoid infinite loops.
*/
const TRANSFORM_DEPTH_LIMIT = 3;
const CONTEXT_SETTINGS = 'settings';
const CONTEXT_STYLES = 'styles';
/**
* @var array<string, Props_Resolver>
*/
private static array $instances = [];
private Transformers_Registry $transformers_registry;
private function __construct( Transformers_Registry $transformers_registry ) {
$this->transformers_registry = $transformers_registry;
}
public static function for_styles(): self {
return self::instance( self::CONTEXT_STYLES );
}
public static function for_settings(): self {
return self::instance( self::CONTEXT_SETTINGS );
}
private static function instance( string $context ): self {
if ( ! isset( self::$instances[ $context ] ) ) {
$instance = new self( new Transformers_Registry() );
self::$instances[ $context ] = $instance;
do_action(
"elementor/atomic-widgets/$context/transformers/register",
$instance->get_transformers_registry(),
$instance
);
}
return self::$instances[ $context ];
}
public static function reset(): void {
self::$instances = [];
}
public function get_transformers_registry(): Transformers_Registry {
return $this->transformers_registry;
}
public function resolve( array $schema, array $props ): array {
$resolved = [];
foreach ( $schema as $key => $prop_type ) {
if ( ! ( $prop_type instanceof Prop_Type ) ) {
continue;
}
$resolved[ $key ] = $props[ $key ] ?? $prop_type->get_default();
}
return $this->assign_values( $resolved, $schema );
}
private function transform( $value, $key, Prop_Type $prop_type, int $depth = 0 ) {
if ( null === $value ) {
return null;
}
if ( ! $this->is_transformable( $value ) ) {
return $value;
}
if ( $depth >= self::TRANSFORM_DEPTH_LIMIT ) {
return null;
}
if ( isset( $value['disabled'] ) && true === $value['disabled'] ) {
return null;
}
if ( $prop_type instanceof Union_Prop_Type ) {
$prop_type = $prop_type->get_prop_type( $value['$$type'] );
if ( ! $prop_type ) {
return null;
}
}
if ( $prop_type instanceof Object_Prop_Type ) {
if ( ! is_array( $value['value'] ) ) {
return null;
}
$value['value'] = $this->resolve(
$prop_type->get_shape(),
$value['value']
);
}
if ( $prop_type instanceof Array_Prop_Type ) {
if ( ! is_array( $value['value'] ) ) {
return null;
}
$value['value'] = $this->assign_values(
$value['value'],
$prop_type->get_item_type()
);
}
$transformer = $this->transformers_registry->get( $value['$$type'] );
if ( ! ( $transformer instanceof Transformer_Base ) ) {
return null;
}
try {
$transformed_value = $transformer->transform( $value['value'], $key );
return $this->transform( $transformed_value, $key, $prop_type, $depth + 1 );
} catch ( Exception $e ) {
return null;
}
}
private function is_transformable( $value ): bool {
return (
! empty( $value['$$type'] ) &&
array_key_exists( 'value', $value )
);
}
private function assign_values( $values, $schema ) {
$assigned = [];
foreach ( $values as $key => $value ) {
$prop_type = $schema instanceof Prop_Type ? $schema : $schema[ $key ];
$transformed = $this->transform( $value, $key, $prop_type );
if ( Multi_Props::is( $transformed ) ) {
$assigned = array_merge( $assigned, Multi_Props::get_value( $transformed ) );
continue;
}
$assigned[ $key ] = $transformed;
}
return $assigned;
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropsResolver;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Transformer_Base {
abstract public function transform( $value, $key );
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropsResolver;
use Elementor\Core\Utils\Collection;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Transformers_Registry extends Collection {
public function register( string $key, Transformer_Base $transformer ): self {
if ( isset( $this->items[ $key ] ) ) {
Utils::safe_throw( "{$key} transformer is already registered." );
return $this;
}
$this->items[ $key ] = $transformer;
return $this;
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropsResolver\Transformers;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformer_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Array_Transformer extends Transformer_Base {
public function transform( $value, $key ) {
if ( ! is_array( $value ) ) {
return null;
}
return array_filter( $value );
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropsResolver\Transformers;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformer_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Combine_Array_Transformer extends Transformer_Base {
private string $separator;
public function __construct( string $separator ) {
$this->separator = $separator;
}
public function transform( $value, $key ) {
if ( ! is_array( $value ) ) {
return null;
}
return implode( $this->separator, array_filter( $value ) );
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropsResolver\Transformers;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformer_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Primitive_Transformer extends Transformer_Base {
public function transform( $value, $key ) {
return $value;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Settings;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformer_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Image_Src_Transformer extends Transformer_Base {
/**
* This transformer (or rather this prop type) exists only to support dynamic images.
* Currently, the dynamic tags that return images return it with id & url no matter
* what, so we need to keep the same structure in the props.
*/
public function transform( $value, $key ) {
return [
'id' => isset( $value['id'] ) ? (int) $value['id'] : null,
'url' => $value['url'] ?? null,
];
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Elementor\Modules\AtomicWidgets\PropsResolver\Transformers\Settings;
use Elementor\Modules\AtomicWidgets\PropsResolver\Transformer_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Image_Transformer extends Transformer_Base {
public function transform( $value, $key ) {
if ( ! empty( $value['src']['id'] ) ) {
$image_src = wp_get_attachment_image_src(
(int) $value['src']['id'],
$value['size'] ?? 'full'
);
if ( ! $image_src ) {
throw new \Exception( 'Cannot get image src.' );
}
[ $src, $width, $height ] = $image_src;
return [
'src' => $src,
'width' => (int) $width,
'height' => (int) $height,
'srcset' => wp_get_attachment_image_srcset( $value['src']['id'], $value['size'] ),
'alt' => get_post_meta( $value['src']['id'], '_wp_attachment_image_alt', true ),
];
}
if ( empty( $value['src']['url'] ) ) {
throw new \Exception( 'Invalid image URL.' );
}
return [
'src' => $value['src']['url'],
];
}
}

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