PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.0-beta3
Elementor Website Builder – more than just a page builder v4.3.0-beta3
4.3.0-beta3 4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 4.0.8 4.1.0-beta1 4.1.0-dev1 All 452 releases
← All changes | modules/global-classes/atomic-global-styles.php +357 -31 4.0.84.3.0-beta3 View file →
@@ -1,87 +1,413 @@
1 1 <?php
2 2
3 3 namespace Elementor\Modules\GlobalClasses;
4 4
5 +use Elementor\Modules\AtomicWidgets\Styles\CacheValidity\Cache_Validity;
5 6 use Elementor\Plugin;
6 7 use Elementor\Modules\AtomicWidgets\Styles\Atomic_Styles_Manager;
7 8
8 9 class Atomic_Global_Styles {
9 10 const STYLES_KEY = 'global';
11 + const RELATED_KEY = 'related';
12 + const RELATED_REVERSE_KEY = 'related-reverse';
10 13
14 + private Global_Classes_Relations $relations;
15 +
16 + public function __construct( Global_Classes_Relations $relations ) {
17 + $this->relations = $relations;
18 + }
19 +
11 20 public function register_hooks() {
12 21 add_action(
13 22 'elementor/atomic-widgets/styles/register',
14 - fn( Atomic_Styles_Manager $styles_manager ) => $this->register_styles( $styles_manager ),
23 + fn( Atomic_Styles_Manager $styles_manager, array $post_ids ) => $this->register_styles( $styles_manager, $post_ids ),
15 24 20,
16 25 2
17 26 );
18 27
19 - add_action( 'elementor/global_classes/update', fn( string $context ) => $this->invalidate_cache( $context ), 10, 1 );
28 + add_action(
29 + 'elementor/global_classes/update',
30 + fn( string $context, array $changes ) => $this->invalidate_cache_for_updated_classes( $context, $changes ),
31 + 10,
32 + 2
33 + );
20 34
21 35 add_action(
22 36 'deleted_post',
23 - fn( $post_id ) => $this->on_post_delete( $post_id )
37 + fn( $post_id ) => $this->on_kit_delete( $post_id )
24 38 );
25 39
26 40 add_action(
27 41 'elementor/core/files/clear_cache',
28 - fn() => $this->invalidate_cache(),
42 + fn() => $this->invalidate_all_cache(),
29 43 );
30 44
31 - add_filter('elementor/atomic-widgets/settings/transformers/classes',
45 + add_filter(
46 + 'elementor/atomic-widgets/settings/transformers/classes',
32 47 fn( $value ) => $this->transform_classes_names( $value )
33 48 );
49 +
50 + add_action(
51 + 'elementor/document/after_save',
52 + fn( $document ) => $this->on_document_save( $document )
53 + );
34 54 }
35 55
36 - private function register_styles( Atomic_Styles_Manager $styles_manager ) {
37 - $context = is_preview() ? Global_Classes_Repository::CONTEXT_PREVIEW : Global_Classes_Repository::CONTEXT_FRONTEND;
56 + private function register_styles( Atomic_Styles_Manager $styles_manager, array $post_ids ) {
57 + $context = $this->get_context();
38 58
39 - $get_styles = function () use ( $context ) {
40 - return Global_Classes_Repository::make()->context( $context )->all()->get_ordered_items()->map( function( $item ) {
41 - $item['id'] = $item['label'];
42 - return $item;
43 - })->reverse()->all(); // we should reverse the order of the items so that the last in the original array should be rendered first (to be overridden by the previous ones)
44 - };
59 + $parent_to_embedded = [];
60 + $visited = [];
45 61
46 - $styles_manager->register(
47 - [ self::STYLES_KEY, $context ],
48 - $get_styles,
49 - );
62 + foreach ( $post_ids as $post_id ) {
63 + $this->resolve_embedded_post_descendants( (int) $post_id, $parent_to_embedded, $visited );
64 + }
65 +
66 + $this->persist_relation_maps( $parent_to_embedded, $context );
67 +
68 + $all_embedded = [];
69 + foreach ( $parent_to_embedded as $children ) {
70 + $all_embedded = array_merge( $all_embedded, $children );
71 + }
72 + $all_embedded = array_flip( array_unique( $all_embedded ) ); // use as set
73 +
74 + foreach ( $post_ids as $post_id ) {
75 + $post_id_int = (int) $post_id;
76 +
77 + if ( isset( $all_embedded[ $post_id_int ] ) ) {
78 + // This post is embedded inside another rendered post – skip it so
79 + // it doesn't produce its own global-{id}-*.css file.
80 + continue;
81 + }
82 +
83 + $embedded_ids = $parent_to_embedded[ $post_id_int ] ?? [];
84 + $aggregate_ids = array_merge( [ $post_id_int ], $embedded_ids );
85 +
86 + $get_styles = fn() => $this->get_document_global_styles( $aggregate_ids, $context );
87 +
88 + $styles_manager->register(
89 + [ $this->get_cache_root_key(), $post_id, $context ],
90 + $get_styles
91 + );
92 + }
50 93 }
51 94
52 - private function on_post_delete( $post_id ) {
95 + /**
96 + * Recursively resolve embedded post descendants for a parent post id.
97 + *
98 + * Applies the `elementor/document/related_posts` filter transitively and
99 + * guards against cycles with the shared $visited set.
100 + *
101 + * @param int $pid Parent post id being inspected.
102 + * @param array<int,int[]> $parent_to_embedded Accumulated forward map.
103 + * @param array<int,true> $visited Cycle guard.
104 + * @return int[] Embedded descendant post ids to merge into $pid's global styles.
105 + */
106 + private function resolve_embedded_post_descendants( int $pid, array &$parent_to_embedded, array &$visited ): array {
107 + if ( isset( $visited[ $pid ] ) ) {
108 + return $parent_to_embedded[ $pid ] ?? [];
109 + }
110 +
111 + $visited[ $pid ] = true;
112 +
113 + $related = (array) apply_filters( 'elementor/document/related_posts', [], $pid );
114 + $related = array_values( array_unique( array_map( 'intval', array_filter( $related, 'is_numeric' ) ) ) );
115 +
116 + $all_related = $related;
117 +
118 + foreach ( $related as $related_post ) {
119 + $further_related_posts = $this->resolve_embedded_post_descendants( $related_post, $parent_to_embedded, $visited );
120 + $all_related = array_values( array_unique( array_merge( $all_related, $further_related_posts ) ) );
121 + }
122 +
123 + $parent_to_embedded[ $pid ] = $all_related;
124 +
125 + return $all_related;
126 + }
127 +
128 + /**
129 + * Returns the merged, ordered global class styles for one or more post ids.
130 + *
131 + * @param int[] $post_ids One or more post ids whose classes should be merged.
132 + * @param string $context Frontend or preview context.
133 + * @return array Ordered style items ready for serialization.
134 + */
135 + private function get_document_global_styles( array $post_ids, string $context ): array {
136 + $is_preview = Global_Classes_Repository::CONTEXT_PREVIEW === $context;
137 +
138 + $class_ids = [];
139 + foreach ( $post_ids as $pid ) {
140 + $ids = $this->relations->set_preview( $is_preview )->get_styles_by_post( (int) $pid );
141 + $class_ids = array_merge( $class_ids, $ids );
142 + }
143 + $class_ids = array_values( array_unique( $class_ids ) );
144 +
145 + if ( empty( $class_ids ) ) {
146 + return [];
147 + }
148 +
149 + $repository = Global_Classes_Repository::make();
150 +
151 + if ( $is_preview ) {
152 + $repository->set_preview( true );
153 + }
154 +
155 + $global_order = $repository->all_labels();
156 + $ordered_class_ids = array_values( array_intersect( array_keys( $global_order ), $class_ids ) );
157 +
158 + if ( empty( $ordered_class_ids ) ) {
159 + return [];
160 + }
161 +
162 + $items = $repository->get_by_ids( $ordered_class_ids );
163 + $reversed_order = array_reverse( $ordered_class_ids );
164 +
165 + $styles = [];
166 +
167 + foreach ( $reversed_order as $class_id ) {
168 + $item = $items[ $class_id ] ?? null;
169 +
170 + if ( ! $item ) {
171 + continue;
172 + }
173 +
174 + $resolved_label = $global_order[ $class_id ] ?? $item['label'];
175 + $item['id'] = $resolved_label;
176 + $item['label'] = $resolved_label;
177 + $styles[] = $item;
178 + }
179 +
180 + return $styles;
181 + }
182 +
183 + /**
184 + * Persist the parent-to-child and child-to-parent relation maps.
185 + *
186 + * @param array<int,int[]> $parent_to_embedded Forward map: parent_id => child_ids[].
187 + */
188 + private function persist_relation_maps( array $parent_to_embedded, string $context ): void {
189 + $cache_validity = new Cache_Validity();
190 +
191 + foreach ( $parent_to_embedded as $parent => $new_children ) {
192 + $forward_path = [ $this->get_cache_root_key( self::RELATED_KEY ), $parent, $context ];
193 +
194 + $old_related_posts = array_map( 'intval', (array) ( $cache_validity->get_meta( $forward_path ) ?? [] ) );
195 + $new_related_posts = array_map( 'intval', $new_children );
196 +
197 + $added_posts = array_diff( $new_related_posts, $old_related_posts );
198 + $removed_posts = array_diff( $old_related_posts, $new_related_posts );
199 +
200 + $cache_validity->validate( $forward_path, $new_related_posts );
201 +
202 + foreach ( $added_posts as $added_post ) {
203 + $this->add_reverse_relation( $cache_validity, $added_post, $parent, $context );
204 + }
205 + foreach ( $removed_posts as $removed_post ) {
206 + $this->remove_reverse_relation( $cache_validity, $removed_post, $parent, $context );
207 + }
208 + }
209 + }
210 +
211 + private function add_reverse_relation( Cache_Validity $cache_validity, int $child, int $parent, string $context ): void {
212 + $reverse_path = [ $this->get_cache_root_key( self::RELATED_REVERSE_KEY ), $child, $context ];
213 +
214 + $existing = array_map( 'intval', (array) ( $cache_validity->get_meta( $reverse_path ) ?? [] ) );
215 +
216 + if ( in_array( $parent, $existing, true ) ) {
217 + return;
218 + }
219 +
220 + $cache_validity->validate( $reverse_path, array_values( array_merge( $existing, [ $parent ] ) ) );
221 + }
222 +
223 + private function remove_reverse_relation( Cache_Validity $cache_validity, int $child, int $parent, string $context ): void {
224 + $reverse_path = [ $this->get_cache_root_key( self::RELATED_REVERSE_KEY ), $child, $context ];
225 +
226 + $existing = array_map( 'intval', (array) ( $cache_validity->get_meta( $reverse_path ) ?? [] ) );
227 + $pruned = array_values( array_filter( $existing, fn( int $p ) => $p !== $parent ) );
228 +
229 + $cache_validity->validate( $reverse_path, $pruned );
230 + }
231 +
232 + /**
233 + * Look up all parent post ids that declared $child_post_id as embedded.
234 + *
235 + * @param int $child_post_id
236 + * @return int[]
237 + */
238 + private function get_parent_post_ids( int $child_post_id, string $context ): array {
239 + $cache_validity = new Cache_Validity();
240 +
241 + $parents = $cache_validity->get_meta( [
242 + $this->get_cache_root_key( self::RELATED_REVERSE_KEY ),
243 + $child_post_id,
244 + $context,
245 + ] );
246 +
247 + if ( ! is_array( $parents ) ) {
248 + return [];
249 + }
250 +
251 + return array_values( array_unique( array_map( 'intval', $parents ) ) );
252 + }
253 +
254 + /**
255 + * Walk the reverse relation map transitively to collect every ancestor
256 + * post that embeds $post_id (direct parent, grandparent, etc.).
257 + *
258 + * @param int $post_id
259 + * @return int[]
260 + */
261 + private function get_ancestor_post_ids( int $post_id, string $context ): array {
262 + $ancestors = [];
263 + $visited = [];
264 + $queue = [ $post_id ];
265 +
266 + while ( ! empty( $queue ) ) {
267 + $current = array_shift( $queue );
268 +
269 + foreach ( $this->get_parent_post_ids( $current, $context ) as $parent_id ) {
270 + if ( isset( $visited[ $parent_id ] ) ) {
271 + continue;
272 + }
273 +
274 + $visited[ $parent_id ] = true;
275 + $ancestors[] = $parent_id;
276 + $queue[] = $parent_id;
277 + }
278 + }
279 +
280 + return array_values( array_unique( array_map( 'intval', $ancestors ) ) );
281 + }
282 +
283 + private function on_kit_delete( $post_id ) {
53 284 if ( ! Plugin::$instance->kits_manager->is_kit( $post_id ) ) {
54 285 return;
55 286 }
56 287
57 - $this->invalidate_cache();
288 + $this->invalidate_all_cache();
58 289 }
59 290
60 - private function invalidate_cache( ?string $context = null ) {
291 + /**
292 + * When an embedded post (component/template) is saved, its own CSS cache
293 + * is cleared by Global_Classes_Relations. We additionally need to clear
294 + * the parent's global CSS so it is regenerated with the updated child content.
295 + */
296 + private function on_document_save( $document ): void {
297 + $post_id = (int) $document->get_main_id();
298 + $context = $this->get_context();
299 +
300 + $cache_validity = new Cache_Validity();
301 + $cache_validity->invalidate( [ $this->get_cache_root_key( self::RELATED_KEY ), $post_id, $context ] );
302 +
303 + foreach ( $this->get_ancestor_post_ids( $post_id, $context ) as $ancestor_id ) {
304 + $this->invalidate_document_cache( $ancestor_id, $context );
305 +
306 + $cache_validity->invalidate( [ $this->get_cache_root_key( self::RELATED_KEY ), $ancestor_id, $context ] );
307 + }
308 + }
309 +
310 + private function invalidate_cache_for_updated_classes( string $context, array $changes ) {
311 + if ( isset( $changes['order'] ) && $changes['order'] ) {
312 + $this->invalidate_all_cache( $context );
313 +
314 + return;
315 + }
316 +
317 + $affected = array_unique( array_merge(
318 + $changes['added'] ?? [],
319 + $changes['deleted'] ?? [],
320 + $changes['modified'] ?? []
321 + ) );
322 +
323 + if ( empty( $affected ) ) {
324 + return;
325 + }
326 +
327 + $document_ids = [];
328 + $is_preview = Global_Classes_Repository::CONTEXT_PREVIEW === $context;
329 +
330 + if ( ! empty( $changes['affected_post_ids'] ) ) {
331 + foreach ( $changes['affected_post_ids'] as $post_id ) {
332 + $document_ids[ (int) $post_id ] = true;
333 + }
334 + }
335 +
336 + foreach ( $affected as $class_id ) {
337 + foreach ( $this->relations->set_preview( $is_preview )->get_posts_by_style( $class_id ) as $doc_id ) {
338 + $document_ids[ $doc_id ] = true;
339 + }
340 + }
341 +
342 + if ( empty( $document_ids ) ) {
343 + return;
344 + }
345 +
346 + // Also include ancestor posts of the directly-affected documents so that
347 + // aggregated CSS bundles are regenerated when a descendant class changes.
348 + $with_parents = $document_ids;
349 + foreach ( array_keys( $document_ids ) as $doc_id ) {
350 + foreach ( $this->get_ancestor_post_ids( (int) $doc_id, $context ) as $ancestor_id ) {
351 + $with_parents[ $ancestor_id ] = true;
352 + }
353 + }
354 +
355 + foreach ( array_keys( $with_parents ) as $post_id ) {
356 + $this->invalidate_document_cache( $post_id, $context );
357 + }
358 + }
359 +
360 + private function invalidate_document_cache( int $post_id, ?string $context = null ) {
361 + if ( empty( $context ) ) {
362 + do_action( 'elementor/atomic-widgets/styles/clear', [ $this->get_cache_root_key(), $post_id ] );
363 + do_action( 'elementor/atomic-widgets/styles/clear', [ $this->get_cache_root_key( self::RELATED_KEY ), $post_id ] );
364 + do_action( 'elementor/atomic-widgets/styles/clear', [ $this->get_cache_root_key( self::RELATED_REVERSE_KEY ), $post_id ] );
365 + } else {
366 + do_action( 'elementor/atomic-widgets/styles/clear', [ $this->get_cache_root_key(), $post_id, $context ] );
367 + do_action( 'elementor/atomic-widgets/styles/clear', [ $this->get_cache_root_key( self::RELATED_KEY ), $post_id, $context ] );
368 + do_action( 'elementor/atomic-widgets/styles/clear', [ $this->get_cache_root_key( self::RELATED_REVERSE_KEY ), $post_id, $context ] );
369 + }
370 + }
371 +
372 + private function invalidate_all_cache( ?string $context = null ) {
61 373 if ( empty( $context ) || Global_Classes_Repository::CONTEXT_FRONTEND === $context ) {
62 - do_action( 'elementor/atomic-widgets/styles/clear', [ self::STYLES_KEY ] );
374 + do_action( 'elementor/atomic-widgets/styles/clear', [ $this->get_cache_root_key() ] );
63 375
376 + $cache_validity = new Cache_Validity();
377 + $cache_validity->invalidate( [ $this->get_cache_root_key( self::RELATED_KEY ) ] );
378 + $cache_validity->invalidate( [ $this->get_cache_root_key( self::RELATED_REVERSE_KEY ) ] );
379 +
64 380 return;
65 381 }
66 382
67 - do_action( 'elementor/atomic-widgets/styles/clear', [ self::STYLES_KEY, $context ] );
383 + do_action( 'elementor/atomic-widgets/styles/clear', [ $this->get_cache_root_key(), $context ] );
68 384 }
69 385
70 386 private function transform_classes_names( $ids ) {
71 - $context = is_preview() ? Global_Classes_Repository::CONTEXT_PREVIEW : Global_Classes_Repository::CONTEXT_FRONTEND;
72 387
73 - $classes = Global_Classes_Repository::make()
74 - ->context( $context )
75 - ->all()
76 - ->get_items();
388 + $labels = Global_Classes_Repository::make()
389 + ->set_preview( $this->is_preview() )
390 + ->all_labels();
77 391
78 392 return array_map(
79 - function( $id ) use ( $classes ) {
80 - $class = $classes->get( $id );
81 -
82 - return $class ? $class['label'] : $id;
393 + static function( $id ) use ( $labels ) {
394 + return $labels[ $id ] ?? $id;
83 395 },
84 396 $ids
85 397 );
398 + }
399 +
400 + private function get_context(): string {
401 + return $this->is_preview()
402 + ? Global_Classes_Repository::CONTEXT_PREVIEW
403 + : Global_Classes_Repository::CONTEXT_FRONTEND;
404 + }
405 +
406 + private function is_preview(): bool {
407 + return Plugin::$instance->preview->is_editor_or_preview();
408 + }
409 +
410 + private function get_cache_root_key( string $key = null ): string {
411 + return $key ? self::STYLES_KEY . '_' . $key : self::STYLES_KEY;
86 412 }
87 413 }