first commit

This commit is contained in:
2026-03-24 00:31:47 +01:00
commit 2506f6f9c7
3328 changed files with 1172155 additions and 0 deletions

View File

@@ -0,0 +1,224 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Styles;
use Elementor\Core\Base\Document;
use Elementor\Core\Breakpoints\Breakpoint;
use Elementor\Core\Utils\Collection;
use Elementor\Modules\AtomicWidgets\Memo;
use Elementor\Modules\AtomicWidgets\Cache_Validity;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Atomic_Styles_Manager {
private static ?self $instance = null;
/**
* @var array<string, array{styles: callable, cache_keys: array<string>}>
*/
private array $registered_styles_by_key = [];
private Cache_Validity $cache_validity;
private array $post_ids = [];
private CSS_Files_Manager $css_files_manager;
const DEFAULT_BREAKPOINT = 'desktop';
private array $fonts = [];
public function __construct() {
$this->css_files_manager = new CSS_Files_Manager();
$this->cache_validity = new Cache_Validity();
}
public static function instance() {
if ( ! self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
public function register_hooks() {
add_action( 'elementor/frontend/after_enqueue_post_styles', fn() => $this->enqueue_styles() );
add_action( 'elementor/post/render', function( $post_id ) {
$this->post_ids[] = $post_id;
} );
}
public function register( string $key, callable $get_style_defs, array $cache_keys ) {
$this->registered_styles_by_key[ $key ] = [
'get_styles' => $get_style_defs,
'cache_keys' => $cache_keys,
];
}
private function enqueue_styles() {
if ( empty( $this->post_ids ) ) {
return;
}
do_action( 'elementor/atomic-widgets/styles/register', $this, $this->post_ids );
$get_styles_memo = new Memo();
$styles_by_key = Collection::make( $this->registered_styles_by_key )
->map_with_keys( fn ( $style_params, $style_key ) => [
$style_key => [
'get_styles' => $get_styles_memo->memoize( $style_key, $style_params['get_styles'] ),
'cache_keys' => $style_params['cache_keys'],
],
])
->all();
$this->before_render( $styles_by_key );
$this->render( $styles_by_key );
$this->after_render( $styles_by_key );
}
private function before_render( array $styles_by_key ) {
$this->fonts = [];
foreach ( $styles_by_key as $style_key => $style_params ) {
$cache_keys = $style_params['cache_keys'];
// This cache validity check is of the general style, and used to reset dependencies that can only be evaluated
// upon the style rendering flow (i.e. when cache is invalid).
// (the corresponding css files cache validity includes also the file's breakpoint in the cache keys array)
if ( ! $this->cache_validity->is_valid( $cache_keys ) ) {
Style_Fonts::make( $style_key )->clear();
}
// We should validate it after this iteration
$this->cache_validity->validate( $cache_keys );
}
}
private function render( array $styles_by_key ) {
$group_by_breakpoint_memo = new Memo();
$breakpoints = $this->get_breakpoints();
foreach ( $breakpoints as $breakpoint_key ) {
foreach ( $styles_by_key as $style_key => $style_params ) {
$cache_keys = $style_params['cache_keys'];
$render_css = fn() => $this->render_css_by_breakpoints( $style_params['get_styles'], $style_key, $breakpoint_key, $group_by_breakpoint_memo );
$breakpoint_media = $this->get_breakpoint_media( $breakpoint_key );
if ( ! $breakpoint_media ) {
continue;
}
$breakpoint_cache_keys = array_merge( $cache_keys, [ $breakpoint_key ] );
$style_file = $this->css_files_manager->get(
$style_key . '-' . $breakpoint_key,
$breakpoint_media,
$render_css,
$this->cache_validity->is_valid( $breakpoint_cache_keys )
);
$this->cache_validity->validate( $breakpoint_cache_keys );
if ( ! $style_file ) {
continue;
}
wp_enqueue_style(
$style_file->get_handle(),
$style_file->get_url(),
[],
$style_file->get_media()
);
}
}
}
private function render_css( array $styles, string $style_key ) {
$style_fonts = Style_Fonts::make( $style_key );
return Styles_Renderer::make(
Plugin::$instance->breakpoints->get_breakpoints_config()
)->on_prop_transform( function( $key, $value ) use ( $style_fonts ) {
if ( 'font-family' !== $key ) {
return;
}
$style_fonts->add( $value );
} )->render( $styles );
}
private function get_breakpoint_media( string $breakpoint_key ): ?string {
$breakpoint_config = Plugin::$instance->breakpoints->get_breakpoints_config()[ $breakpoint_key ] ?? null;
return $breakpoint_config ? Styles_Renderer::get_media_query( $breakpoint_config ) : 'all';
}
private function render_css_by_breakpoints( callable $get_styles, string $style_key, string $breakpoint_key, Memo $group_by_breakpoint_memo ) {
$memo_key = $style_key . '-' . $breakpoint_key;
$get_grouped_styles = $group_by_breakpoint_memo->memoize( $memo_key, fn() => $this->group_by_breakpoint( $get_styles() ) );
$grouped_styles = $get_grouped_styles();
return $this->render_css( $grouped_styles[ $breakpoint_key ] ?? [], $style_key );
}
private function group_by_breakpoint( $styles ) {
return Collection::make( $styles )->reduce( function( $group, $style ) {
Collection::make( $style['variants'] )->each( function( $variant ) use ( &$group, $style ) {
$breakpoint = $variant['meta']['breakpoint'] ?? self::DEFAULT_BREAKPOINT;
if ( ! isset( $group[ $breakpoint ][ $style['id'] ] ) ) {
$group[ $breakpoint ][ $style['id'] ] = [
'id' => $style['id'],
'type' => $style['type'],
'variants' => [],
];
}
$group[ $breakpoint ][ $style['id'] ]['variants'][] = $variant;
} );
return $group;
}, [] );
}
private function get_breakpoints() {
return Collection::make( Plugin::$instance->breakpoints->get_breakpoints() )
->map( fn( Breakpoint $breakpoint ) => $breakpoint->get_name() )
->reverse()
->prepend( self::DEFAULT_BREAKPOINT )
->all();
}
private function after_render( array $styles_by_key ) {
foreach ( $styles_by_key as $style_key => $style_params ) {
$this->add_fonts_to_enqueue( $style_key );
}
$this->enqueue_fonts();
}
private function add_fonts_to_enqueue( string $style_key ) {
$style_fonts = Style_Fonts::make( $style_key );
$this->fonts = array_unique(
array_merge(
$this->fonts,
array_values( $style_fonts->get() )
)
);
}
private function enqueue_fonts() {
foreach ( $this->fonts as $font ) {
Plugin::instance()->frontend->enqueue_font( $font );
}
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Styles;
use Elementor\Core\Utils\Collection;
use Elementor\Modules\AtomicWidgets\Cache_Validity;
use Elementor\Modules\AtomicWidgets\Utils;
use Elementor\Plugin;
class Atomic_Widget_Base_Styles {
const STYLES_KEY = 'base';
public function register_hooks() {
add_action(
'elementor/atomic-widgets/styles/register',
fn( Atomic_Styles_Manager $styles_manager ) => $this->register_styles( $styles_manager ),
10,
1
);
add_action(
'elementor/core/files/clear_cache',
fn() => $this->invalidate_cache(),
);
}
private function register_styles( Atomic_Styles_Manager $styles_manager ) {
$styles_manager->register(
self::STYLES_KEY,
fn () => $this->get_all_base_styles(),
[ self::STYLES_KEY ]
);
}
private function invalidate_cache() {
$cache_validity = new Cache_Validity();
$cache_validity->invalidate( [ self::STYLES_KEY ] );
}
public function get_all_base_styles(): array {
$elements = Plugin::$instance->elements_manager->get_element_types();
$widgets = Plugin::$instance->widgets_manager->get_widget_types();
return Collection::make( $elements )
->merge( $widgets )
->filter( fn( $element ) => Utils::is_atomic( $element ) )
->map( fn( $element ) => $element->get_base_styles() )
->flatten()
->all();
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Styles;
use Elementor\Core\Base\Document;
use Elementor\Modules\AtomicWidgets\Cache_Validity;
use Elementor\Modules\AtomicWidgets\Utils;
use Elementor\Modules\GlobalClasses\Utils\Atomic_Elements_Utils;
use Elementor\Plugin;
class Atomic_Widget_Styles {
const STYLES_KEY = 'local';
public function register_hooks() {
add_action( 'elementor/atomic-widgets/styles/register', function( Atomic_Styles_Manager $styles_manager, array $post_ids ) {
$this->register_styles( $styles_manager, $post_ids );
}, 30, 2 );
add_action( 'elementor/document/after_save', fn( Document $document ) => $this->invalidate_cache(
[ $document->get_main_post()->ID ]
), 20, 2 );
add_action(
'elementor/core/files/clear_cache',
fn() => $this->invalidate_cache(),
);
}
private function register_styles( Atomic_Styles_Manager $styles_manager, array $post_ids ) {
foreach ( $post_ids as $post_id ) {
$get_styles = fn() => $this->parse_post_styles( $post_id );
$style_key = $this->get_style_key( $post_id );
$styles_manager->register( $style_key, $get_styles, [ self::STYLES_KEY, $post_id ] );
}
}
private function parse_post_styles( $post_id ) {
$post_styles = [];
Utils::traverse_post_elements( $post_id, function( $element_data ) use ( &$post_styles ) {
$post_styles = array_merge( $post_styles, $this->parse_element_style( $element_data ) );
} );
return $post_styles;
}
private function parse_element_style( array $element_data ) {
$element_type = Atomic_Elements_Utils::get_element_type( $element_data );
$element_instance = Atomic_Elements_Utils::get_element_instance( $element_type );
if ( ! Utils::is_atomic( $element_instance ) ) {
return [];
}
return $element_data['styles'] ?? [];
}
private function invalidate_cache( ?array $post_ids = null ) {
$cache_validity = new Cache_Validity();
if ( empty( $post_ids ) ) {
$cache_validity->invalidate( [ self::STYLES_KEY ] );
return;
}
foreach ( $post_ids as $post_id ) {
$cache_validity->invalidate( [ self::STYLES_KEY, $post_id ] );
}
}
private function get_style_key( $post_id ) {
return self::STYLES_KEY . '-' . $post_id;
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Styles;
class CSS_Files_Manager {
const DEFAULT_CSS_DIR = 'elementor/css/';
const FILE_EXTENSION = '.css';
// Read and write permissions for the owner
const PERMISSIONS = 0644;
public function get( string $handle, string $media, callable $get_css, bool $is_valid_cache ): ?Style_File {
$filesystem = $this->get_filesystem();
$path = $this->get_path( $handle );
if ( $is_valid_cache ) {
if ( ! $filesystem->exists( $path ) ) {
return null;
}
// Return the existing file
return Style_File::create(
$this->sanitize_handle( $handle ),
$this->get_filesystem_path( $this->get_path( $handle ) ),
$this->get_url( $handle ),
$media,
);
}
$css = $get_css();
if ( empty( $css ) ) {
return null;
}
$filesystem_path = $this->get_filesystem_path( $path );
$is_created = $filesystem->put_contents( $filesystem_path, $css, self::PERMISSIONS );
if ( false === $is_created ) {
return null;
}
return Style_File::create(
$this->sanitize_handle( $handle ),
$filesystem_path,
$this->get_url( $handle ),
$media
);
}
private function get_filesystem(): \WP_Filesystem_Base {
global $wp_filesystem;
if ( empty( $wp_filesystem ) ) {
require_once ABSPATH . '/wp-admin/includes/file.php';
WP_Filesystem();
}
return $wp_filesystem;
}
private function get_filesystem_path( $path ): string {
$filesystem = $this->get_filesystem();
return str_replace( ABSPATH, $filesystem->abspath(), $path );
}
private function get_url( string $handle ): string {
$upload_dir = wp_upload_dir();
$sanitized_handle = $this->sanitize_handle( $handle );
$handle = $sanitized_handle . self::FILE_EXTENSION;
return trailingslashit( $upload_dir['baseurl'] ) . self::DEFAULT_CSS_DIR . $handle;
}
private function get_path( string $handle ): string {
$upload_dir = wp_upload_dir();
$sanitized_handle = $this->sanitize_handle( $handle );
$handle = $sanitized_handle . self::FILE_EXTENSION;
return trailingslashit( $upload_dir['basedir'] ) . self::DEFAULT_CSS_DIR . $handle;
}
private function sanitize_handle( string $handle ): string {
return preg_replace( '/[^a-zA-Z0-9_-]/', '', $handle );
}
}

View File

@@ -0,0 +1,129 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Styles;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Size_Constants {
const UNIT_PX = 'px';
const UNIT_PERCENT = '%';
const UNIT_EM = 'em';
const UNIT_REM = 'rem';
const UNIT_VW = 'vw';
const UNIT_VH = 'vh';
const UNIT_AUTO = 'auto';
const UNIT_CUSTOM = 'custom';
const UNIT_SECOND = 's';
const UNIT_MILLI_SECOND = 'ms';
const UNIT_ANGLE_DEG = 'deg';
const LENGTH_UNITS = [
self::UNIT_PX,
self::UNIT_EM,
self::UNIT_REM,
self::UNIT_VW,
self::UNIT_VH,
];
const TIME_UNITS = [ self::UNIT_SECOND, self::UNIT_MILLI_SECOND ];
const EXTENDED_UNITS = [ self::UNIT_AUTO, self::UNIT_CUSTOM ];
const VIEWPORT_MIN_MAX_UNITS = [ 'vmin', 'vmax' ];
const ANGLE_UNITS = [ self::UNIT_ANGLE_DEG, 'rad', 'grad', 'turn' ];
public static function all_supported_units(): array {
return array_merge(
self::all(),
self::ANGLE_UNITS,
self::TIME_UNITS,
self::EXTENDED_UNITS,
self::VIEWPORT_MIN_MAX_UNITS,
);
}
public static function all(): array {
return [
...self::LENGTH_UNITS,
self::UNIT_PERCENT,
self::UNIT_AUTO,
];
}
private static function units_without_auto(): array {
return [ ...self::LENGTH_UNITS, self::UNIT_PERCENT ];
}
public static function layout() {
return self::units_without_auto();
}
public static function spacing(): array {
return self::units_without_auto();
}
public static function position(): array {
return self::units_without_auto();
}
public static function anchor_offset() {
return self::LENGTH_UNITS;
}
public static function typography(): array {
return self::units_without_auto();
}
public static function stroke_width() {
return [
self::UNIT_PX,
self::UNIT_EM,
self::UNIT_REM,
];
}
public static function transition() {
return self::TIME_UNITS;
}
public static function border(): array {
return self::units_without_auto();
}
public static function opacity(): array {
return [ self::UNIT_PERCENT ];
}
public static function box_shadow(): array {
return self::units_without_auto();
}
public static function rotate(): array {
return self::ANGLE_UNITS;
}
public static function transform(): array {
return self::units_without_auto();
}
public static function drop_shadow() {
return self::LENGTH_UNITS;
}
public static function blur_filter() {
return self::LENGTH_UNITS;
}
public static function intensity_filter() {
return [ self::UNIT_PERCENT ];
}
public static function color_tone_filter() {
return [ self::UNIT_PERCENT ];
}
public static function hue_rotate_filter() {
return self::ANGLE_UNITS;
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Styles;
class Style_Definition {
private string $type = 'class';
private string $label = '';
/** @var Style_Variant[] */
private array $variants = [];
public static function make(): self {
return new self();
}
public function set_type( string $type ): self {
$this->type = $type;
return $this;
}
public function set_label( string $label ): self {
$this->label = $label;
return $this;
}
public function add_variant( Style_Variant $variant ): self {
$this->variants[] = $variant->build();
return $this;
}
public function build( string $id ): array {
return [
'id' => $id,
'type' => $this->type,
'label' => $this->label,
'variants' => $this->variants,
];
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Styles;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Style_File {
private string $handle;
private string $path;
private string $url;
private string $media;
private function __construct( string $handle, string $path, string $url, string $media ) {
$this->handle = $handle;
$this->path = $path;
$this->url = $url;
$this->media = $media;
}
public static function create( string $handle, string $path, string $url, string $media ): self {
return new self( $handle, $path, $url, $media );
}
public function get_handle(): string {
return $this->handle;
}
public function get_path(): string {
return $this->path;
}
public function get_url(): string {
return $this->url;
}
public function get_media(): string {
return $this->media;
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Styles;
use Elementor\Core\Base\Document;
use Elementor\Core\Breakpoints\Breakpoint;
use Elementor\Core\Utils\Collection;
use Elementor\Modules\AtomicWidgets\Memo;
use Elementor\Modules\AtomicWidgets\Cache_Validity;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
const FONTS_KEY_PREFIX = 'elementor_atomic_styles_fonts-';
class Style_Fonts {
private string $style_key;
public function __construct( string $style_key ) {
$this->style_key = $style_key;
}
public static function make( string $style_key ) {
return new static( $style_key );
}
public function add( string $font ) {
$style_fonts = $this->get_fonts();
if ( ! in_array( $font, $style_fonts, true ) ) {
$style_fonts[] = $font;
$this->update_fonts( $style_fonts );
}
}
public function get(): array {
return $this->get_fonts();
}
public function clear() {
$this->update_fonts( [] );
}
private function get_fonts(): array {
$style_fonts_key = $this->get_key();
return get_option( $style_fonts_key, [] );
}
private function update_fonts( array $fonts ) {
$style_fonts_key = $this->get_key();
update_option( $style_fonts_key, $fonts );
}
private function get_key(): string {
return FONTS_KEY_PREFIX . $this->style_key;
}
}

View File

@@ -0,0 +1,326 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Styles;
use Elementor\Modules\AtomicWidgets\DynamicTags\Dynamic_Prop_Types_Mapping;
use Elementor\Modules\AtomicWidgets\PropTypes\Background_Image_Overlay_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Background_Overlay_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Background_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Box_Shadow_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Border_Radius_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Border_Width_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Color_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Dimensions_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Filters\Backdrop_Filter_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Filters\Filter_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Layout_Direction_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Position_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\Number_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\Transform\Transform_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Transition_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Union_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropTypes\Flex_Prop_Type;
use Elementor\Modules\AtomicWidgets\PropDependencies\Manager as Dependency_Manager;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Style_Schema {
public static function get() {
return apply_filters( 'elementor/atomic-widgets/styles/schema', static::get_style_schema() );
}
public static function get_style_schema(): array {
return array_merge(
self::get_size_props(),
self::get_position_props(),
self::get_typography_props(),
self::get_spacing_props(),
self::get_border_props(),
self::get_background_props(),
self::get_effects_props(),
self::get_layout_props(),
self::get_alignment_props(),
);
}
private static function get_size_props() {
return [
'width' => Size_Prop_Type::make(),
'height' => Size_Prop_Type::make(),
'min-width' => Size_Prop_Type::make(),
'min-height' => Size_Prop_Type::make(),
'max-width' => Size_Prop_Type::make(),
'max-height' => Size_Prop_Type::make(),
'overflow' => String_Prop_Type::make()->enum( [
'visible',
'hidden',
'auto',
] ),
'aspect-ratio' => String_Prop_Type::make(),
'object-fit' => String_Prop_Type::make()->enum( [
'fill',
'cover',
'contain',
'none',
'scale-down',
] ),
'object-position' => Union_Prop_Type::make()
->add_prop_type( String_Prop_Type::make()->enum( Position_Prop_Type::get_position_enum_values() ) )
->add_prop_type( Position_Prop_Type::make() )
->set_dependencies(
Dependency_Manager::make( Dependency_Manager::RELATION_AND )
->where( [
'operator' => 'ne',
'path' => [ 'object-fit' ],
'value' => 'fill',
] )
->where( [
'operator' => 'exists',
'path' => [ 'object-fit' ],
] )
->get()
),
];
}
private static function get_position_props() {
return [
'position' => String_Prop_Type::make()->enum( [
'static',
'relative',
'absolute',
'fixed',
'sticky',
] ),
'inset-block-start' => Size_Prop_Type::make(),
'inset-inline-end' => Size_Prop_Type::make(),
'inset-block-end' => Size_Prop_Type::make(),
'inset-inline-start' => Size_Prop_Type::make(),
'z-index' => Number_Prop_Type::make(),
'scroll-margin-top' => Size_Prop_Type::make()->units( Size_Constants::anchor_offset() ),
];
}
private static function get_typography_props() {
return [
'font-family' => String_Prop_Type::make(),
'font-weight' => String_Prop_Type::make()->enum( [
'100',
'200',
'300',
'400',
'500',
'600',
'700',
'800',
'900',
'normal',
'bold',
'bolder',
'lighter',
] ),
'font-size' => Size_Prop_Type::make()->units( Size_Constants::typography() ),
'color' => Color_Prop_Type::make(),
'letter-spacing' => Size_Prop_Type::make()->units( Size_Constants::typography() ),
'word-spacing' => Size_Prop_Type::make()->units( Size_Constants::typography() ),
'column-count' => Number_Prop_Type::make(),
'column-gap' => Size_Prop_Type::make()
->set_dependencies(
Dependency_Manager::make()
->where( [
'operator' => 'gte',
'path' => [ 'column-count' ],
'value' => 1,
] )
->get()
),
'line-height' => Size_Prop_Type::make()->units( Size_Constants::typography() ),
'text-align' => String_Prop_Type::make()->enum( [
'start',
'center',
'end',
'justify',
] ),
'font-style' => String_Prop_Type::make()->enum( [
'normal',
'italic',
'oblique',
] ),
// TODO: validate text-decoration in more specific way [EDS-524]
'text-decoration' => String_Prop_Type::make(),
'text-transform' => String_Prop_Type::make()->enum( [
'none',
'capitalize',
'uppercase',
'lowercase',
] ),
'direction' => String_Prop_Type::make()->enum( [
'ltr',
'rtl',
] ),
'stroke' => Stroke_Prop_Type::make(),
'all' => String_Prop_Type::make()->enum( [
'initial',
'inherit',
'unset',
'revert',
'revert-layer',
] ),
'cursor' => String_Prop_Type::make()->enum( [
'pointer',
] ),
];
}
private static function get_spacing_props() {
return [
'padding' => Union_Prop_Type::make()
->add_prop_type( Dimensions_Prop_Type::make_with_units( Size_Constants::spacing() ) )
->add_prop_type( Size_Prop_Type::make()->units( Size_Constants::spacing() ) ),
'margin' => Union_Prop_Type::make()
->add_prop_type( Dimensions_Prop_Type::make() )
->add_prop_type( Size_Prop_Type::make() ),
];
}
private static function get_border_props() {
return [
'border-radius' => Union_Prop_Type::make()
->add_prop_type( Size_Prop_Type::make()->units( Size_Constants::border() ) )
->add_prop_type( Border_Radius_Prop_Type::make() ),
'border-width' => Union_Prop_Type::make()
->add_prop_type( Size_Prop_Type::make()->units( Size_Constants::border() ) )
->add_prop_type( Border_Width_Prop_Type::make() ),
'border-color' => Color_Prop_Type::make(),
'border-style' => String_Prop_Type::make()->enum( [
'none',
'hidden',
'dotted',
'dashed',
'solid',
'double',
'groove',
'ridge',
'inset',
'outset',
] ),
];
}
private static function get_background_props() {
// Background image overlay as an exception
$background_prop_type = Background_Prop_Type::make();
$bg_overlay_prop_type = $background_prop_type->get_shape_field( Background_Overlay_Prop_Type::get_key() );
$bg_image_overlay_prop_type = $bg_overlay_prop_type->get_item_type()->get_prop_type( Background_Image_Overlay_Prop_Type::get_key() );
Dynamic_Prop_Types_Mapping::make()->get_modified_prop_types( $bg_image_overlay_prop_type->get_shape() );
return [
'background' => $background_prop_type,
];
}
private static function get_effects_props() {
return [
'box-shadow' => Box_Shadow_Prop_Type::make(),
'opacity' => Size_Prop_Type::make()
->units( Size_Constants::opacity() )
->default_unit( Size_Constants::UNIT_PERCENT ),
'filter' => Filter_Prop_Type::make(),
'backdrop-filter' => Backdrop_Filter_Prop_Type::make(),
'transform' => Transform_Prop_Type::make(),
'transition' => Transition_Prop_Type::make(),
];
}
private static function get_layout_props() {
return [
'display' => String_Prop_Type::make()->enum( [
'block',
'inline',
'inline-block',
'flex',
'inline-flex',
'grid',
'inline-grid',
'flow-root',
'none',
'contents',
] ),
'flex-direction' => String_Prop_Type::make()->enum( [
'row',
'row-reverse',
'column',
'column-reverse',
] ),
'gap' => Union_Prop_Type::make()
->add_prop_type( Layout_Direction_Prop_Type::make() )
->add_prop_type( Size_Prop_Type::make()->units( Size_Constants::layout() ) ),
'flex-wrap' => String_Prop_Type::make()->enum( [
'wrap',
'nowrap',
'wrap-reverse',
] ),
'flex' => Flex_Prop_Type::make(),
];
}
private static function get_alignment_props() {
return [
'justify-content' => String_Prop_Type::make()->enum( [
'center',
'start',
'end',
'flex-start',
'flex-end',
'left',
'right',
'normal',
'space-between',
'space-around',
'space-evenly',
'stretch',
] ),
'align-content' => String_Prop_Type::make()->enum( [
'center',
'start',
'end',
'space-between',
'space-around',
'space-evenly',
] ),
'align-items' => String_Prop_Type::make()->enum( [
'normal',
'stretch',
'center',
'start',
'end',
'flex-start',
'flex-end',
'self-start',
'self-end',
'anchor-center',
] ),
'align-self' => String_Prop_Type::make()->enum( [
'auto',
'normal',
'center',
'start',
'end',
'self-start',
'self-end',
'flex-start',
'flex-end',
'anchor-center',
'baseline',
'first baseline',
'last baseline',
'stretch',
] ),
'order' => Number_Prop_Type::make(),
];
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Styles;
class Style_Variant {
private ?string $breakpoint = null;
private ?string $state = null;
/** @var array<string, array> */
private array $props = [];
public static function make(): self {
return new self();
}
public function set_breakpoint( string $breakpoint ): self {
$this->breakpoint = $breakpoint;
return $this;
}
public function set_state( string $state ): self {
$this->state = $state;
return $this;
}
public function add_prop( string $key, $value ): self {
$this->props[ $key ] = $value;
return $this;
}
public function build(): array {
return [
'meta' => [
'breakpoint' => $this->breakpoint,
'state' => $this->state,
],
'props' => $this->props,
];
}
}

View File

@@ -0,0 +1,190 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Styles;
use Elementor\Core\Utils\Collection;
use Elementor\Modules\AtomicWidgets\Module;
use Elementor\Plugin;
use Elementor\Utils;
use Elementor\Modules\AtomicWidgets\PropsResolver\Render_Props_Resolver;
class Styles_Renderer {
const DEFAULT_SELECTOR_PREFIX = '.elementor';
/**
* @var array<string, array{direction: 'min' | 'max', value: int, is_enabled: boolean}>
*/
private array $breakpoints;
private $on_prop_transform;
private string $selector_prefix;
/**
* @param array<string, array{direction: 'min' | 'max', value: int, is_enabled: boolean}> $breakpoints
* @param string $selector_prefix
*/
private function __construct( array $breakpoints, string $selector_prefix = self::DEFAULT_SELECTOR_PREFIX ) {
$this->breakpoints = $breakpoints;
$this->selector_prefix = $selector_prefix;
}
public static function make( array $breakpoints, string $selector_prefix = self::DEFAULT_SELECTOR_PREFIX ): self {
return new self( $breakpoints, $selector_prefix );
}
/**
* Render the styles to a CSS string.
*
* Styles format:
* array<int, array{
* id: string,
* type: string,
* cssName: string | null,
* variants: array<int, array{
* props: array<string, mixed>,
* meta: array<string, mixed>
* }>
* }>
*
* @param array $styles Array of style definitions.
*
* @return string Rendered CSS string.
*/
public function render( array $styles ): string {
$css_style = [];
foreach ( $styles as $style_def ) {
$style = $this->style_definition_to_css_string( $style_def );
$css_style[] = $style;
}
return implode( '', $css_style );
}
public function on_prop_transform( callable $callback ): self {
$this->on_prop_transform = $callback;
return $this;
}
private function style_definition_to_css_string( array $style ): string {
$base_selector = $this->get_base_selector( $style );
if ( ! $base_selector ) {
return '';
}
$stylesheet = [];
foreach ( $style['variants'] as $variant ) {
$style_declaration = $this->variant_to_css_string( $base_selector, $variant );
if ( $style_declaration ) {
$stylesheet[] = $style_declaration;
}
}
return implode( '', $stylesheet );
}
private function get_base_selector( array $style_def ): ?string {
$map = [
'class' => '.',
];
if (
isset( $style_def['type'] ) &&
isset( $style_def['id'] ) &&
isset( $map[ $style_def['type'] ] ) &&
$style_def['id']
) {
$type = $map[ $style_def['type'] ];
$name = $style_def['cssName'] ?? $style_def['id'];
$selector_parts = array_filter( [
$this->selector_prefix,
"{$type}{$name}",
] );
return implode( ' ', $selector_parts );
}
return null;
}
private function variant_to_css_string( string $base_selector, array $variant ): string {
$css = $this->props_to_css_string( $variant['props'] ) ?? '';
$custom_css = $this->custom_css_to_css_string( $variant['custom_css'] ?? null );
if ( ! $css && ! $custom_css ) {
return '';
}
$state = isset( $variant['meta']['state'] ) ? ':' . $variant['meta']['state'] : '';
$selector = $base_selector . $state;
$style_declaration = $selector . '{' . $css . $custom_css . '}';
if ( isset( $variant['meta']['breakpoint'] ) ) {
$style_declaration = $this->wrap_with_media_query( $variant['meta']['breakpoint'], $style_declaration );
}
return $style_declaration;
}
private function props_to_css_string( array $props ): string {
$schema = Style_Schema::get();
return Collection::make( Render_Props_Resolver::for_styles()->resolve( $schema, $props ) )
->filter()
->map( function ( $value, $prop ) {
if ( $this->on_prop_transform ) {
call_user_func( $this->on_prop_transform, $prop, $value );
}
return $prop . ':' . $value . ';';
} )
->implode( '' );
}
private function custom_css_to_css_string( ?array $custom_css ): string {
$is_feature_active = Plugin::$instance->experiments->is_feature_active( Module::EXPERIMENT_CUSTOM_CSS );
return $is_feature_active && ! empty( $custom_css['raw'] )
? Utils::decode_string( $custom_css['raw'], '' ) . '\n'
: '';
}
private function wrap_with_media_query( string $breakpoint_id, string $css ): string {
if ( ! isset( $this->breakpoints[ $breakpoint_id ] ) ) {
return $css;
}
$breakpoint = $this->breakpoints[ $breakpoint_id ];
if ( isset( $breakpoint['is_enabled'] ) && ! $breakpoint['is_enabled'] ) {
return '';
}
$query = $this->get_media_query( $this->breakpoints[ $breakpoint_id ] );
return $query ? $query . '{' . $css . '}' : $css;
}
public static function get_media_query( $breakpoint ): ?string {
if ( isset( $breakpoint['is_enabled'] ) && ! $breakpoint['is_enabled'] ) {
return null;
}
$size = self::get_breakpoint_size( $breakpoint );
return $size ? '@media(' . $size . ')' : null;
}
private static function get_breakpoint_size( array $breakpoint ): ?string {
$bound = 'min' === $breakpoint['direction'] ? 'min-width' : 'max-width';
$width = $breakpoint['value'] . 'px';
return "{$bound}:{$width}";
}
}