PluginProbe
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder / 2.14.0
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder v2.14.0
2.14.0 2.13.0 2.13.1 2.12.0 2.11.1 2.11.0 2.10.0 2.9.0 2.7.4 2.7.5 2.7.6 2.7.7 2.8.0 2.8.1 2.9.1 trunk 1.0 1.0-beta1 1.0-beta2 1.0-beta3 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 All 81 releases
← All changes | includes/classes/atomic-styles.php +202 -9 2.13.1 → 2.14.0 View file →
@@ -22,8 +22,26 @@
22 22 */
23 23 class AtomicStyles {
24 24
25 25 /**
26 + * Revision of the CSS this compiler emits. Bump it whenever the emitted
27 + * output changes for the same stored styles.
28 + *
29 + * Front-end pages do not compile per request: each page is baked once
30 + * into an uploads stylesheet, and Assets::build_revision() decides when
31 + * that file is stale. It was keyed on ABLOCKS_VERSION alone, so an
32 + * emission change with no version bump never reached pages baked before
33 + * it. The centring-margin fix (emitted_value() / flex_child_css()) showed
34 + * up in the editor, which compiles live, while every existing page kept
35 + * serving the old `margin-left:auto` and its wide flex-row gap.
36 + *
37 + * 2: centring margins resolved per parent layout.
38 + * 3: a bucket that sets a border width/colour but no type keeps the type it
39 + * inherits instead of `solid`.
40 + */
41 + const OUTPUT_REVISION = 3;
42 +
43 + /**
26 44 * Compile a full bucket tree for one selector base into CSS.
27 45 *
28 46 * The stored state key IS the pseudo-selector, so it is appended directly;
29 47 * the breakpoint comes from the bucket's device via the one media-query
@@ -52,11 +70,14 @@
52 70 $rules = [];
53 71 $devices = Helper::get_responsive_devices();
54 72 $has_styles = StyleBuckets::has_schema_version( $styles );
55 73
56 - foreach ( $devices as $device ) {
74 + $devices = array_values( $devices );
75 +
76 + foreach ( $devices as $index => $device ) {
57 77 $media = Helper::breakpoint_media_query( $device );
58 78 $bucket_key = StyleBuckets::device_bucket_key( $device );
79 + $cascade = self::cascade_devices( $devices, $index );
59 80
60 81 foreach ( StyleBuckets::state_keys() as $state ) {
61 82 $declarations = [];
62 83
@@ -62,10 +83,14 @@
62 83
63 84 if ( $has_styles ) {
64 85 $props = StyleBuckets::read_bucket( $styles, $bucket_key, $state );
65 86 if ( ! empty( $props ) ) {
66 - $declarations = self::apply_background_reset(
67 - self::state_declarations( $props ),
87 + // The border type in force from the rest of the cascade,
88 + // so a bucket that only changes width/colour keeps it
89 + // rather than falling back to `solid`.
90 + $inherited_style = StyleBuckets::inherited_prop( $styles, $bucket_key, $state, 'borderStyle', $cascade );
91 + $declarations = self::apply_background_reset(
92 + self::state_declarations( $props, is_scalar( $inherited_style ) ? (string) $inherited_style : '' ),
68 93 '' === $state && '' === $bucket_key
69 94 );
70 95 }
71 96 }
@@ -100,8 +125,35 @@
100 125 return $rules;
101 126 }
102 127
103 128 /**
129 + * The devices a device's rules cascade from on the page: those that also
130 + * apply at its width and are emitted before it, widest-first, ending at the
131 + * device itself. Mirror of the JS `cascadeDevices()` (atomic-shared/hash.js),
132 + * which builds it from `devicesApplyingAt( representativeWidth() )`.
133 + *
134 + * @param array $devices Ordered device list (widest-first, base first).
135 + * @param int $index The device's position in it.
136 + * @return array Devices to inherit from.
137 + */
138 + private static function cascade_devices( $devices, $index ) {
139 + $own = (array) $devices[ $index ];
140 + // A device's representative width: its upper bound, else its lower
141 + // bound, else "very wide" for the base device.
142 + $width = ! empty( $own['max'] ) ? (int) $own['max'] : ( ! empty( $own['min'] ) ? (int) $own['min'] : 99999 );
143 +
144 + $out = [];
145 + foreach ( array_slice( $devices, 0, $index + 1 ) as $device ) {
146 + list( $min, $max ) = Helper::breakpoint_bounds( $device );
147 + if ( ( $min > 0 && $width < $min ) || ( $max > 0 && $width > $max ) ) {
148 + continue;
149 + }
150 + $out[] = $device;
151 + }
152 + return $out;
153 + }
154 +
155 + /**
104 156 * FNV-1a 32-bit over the normalised rule list.
105 157 *
106 158 * Deliberately not md5: the editor has to compute the identical hash at save
107 159 * time, and FNV-1a is a handful of lines in both languages rather than a
@@ -159,8 +211,45 @@
159 211
160 212 return [ 'class' => $class, 'css' => self::rules_to_css( $rules, '.' . $class . '.' . $class ) ];
161 213 }
162 214
215 + /**
216 + * Point a block's saved markup at its current style class.
217 + *
218 + * The editor stamps `ablocks-s-{hash}` into the markup at save time, but the
219 + * front end emits rules for the hash of what the compiler produces NOW. When
220 + * the compiled output changes for the same stored styles (OUTPUT_REVISION),
221 + * a post saved before carries a class no rule targets any more and the
222 + * block renders unstyled until someone re-saves it. Only the block's own
223 + * opening tag is touched — inner blocks are rendered, and fixed, on their
224 + * own — and only when it lacks the current class.
225 + *
226 + * @param string $content The block's saved markup.
227 + * @param array $styles The block's styles attribute.
228 + * @param array $alignment The block's alignment attribute.
229 + * @return string The markup, with a stale style class replaced.
230 + */
231 + public static function refresh_style_class( $content, $styles, $alignment = [] ) {
232 + if ( ! is_string( $content ) || false === strpos( $content, 'ablocks-s-' ) ) {
233 + return $content;
234 + }
235 + if ( ! preg_match( '/^\s*<[a-zA-Z][^>]*>/', $content, $tag ) ) {
236 + return $content;
237 + }
238 + $open = $tag[0];
239 + if ( ! preg_match( '/(?<=[\s"\'])ablocks-s-[0-9a-z]+(?=[\s"\'])/', $open, $stale ) ) {
240 + return $content;
241 + }
242 +
243 + $current = self::style_class( $styles, $alignment );
244 + if ( '' === $current || preg_match( '/(?<=[\s"\'])' . preg_quote( $current, '/' ) . '(?=[\s"\'])/', $open ) ) {
245 + return $content;
246 + }
247 +
248 + $fixed = preg_replace( '/(?<=[\s"\'])' . preg_quote( $stale[0], '/' ) . '(?=[\s"\'])/', $current, $open, 1 );
249 + return substr_replace( $content, $fixed, strpos( $content, $open ), strlen( $open ) );
250 + }
251 +
163 252 /** Render a normalised rule list against a selector base. */
164 253 public static function rules_to_css( $rules, $selector_base ) {
165 254 $css = '';
166 255 foreach ( $rules as $rule ) {
@@ -171,9 +260,9 @@
171 260 // — scalar, range, colour, typography, effect, overlay — passes
172 261 // through one guard on its way out. This runs after style_hash()
173 262 // has already read $rules, so the hash the editor writes into
174 263 // the markup is unaffected.
175 - $declarations .= Helper::esc_css_value( $pair[0] ) . ':' . Helper::esc_css_value( $pair[1] ) . ';';
264 + $declarations .= Helper::esc_css_value( $pair[0] ) . ':' . Helper::esc_css_value( self::emitted_value( $pair[0], $pair[1] ) ) . ';';
176 265 }
177 266 $body = $selector_base . $state . '{' . $declarations . '}';
178 267
179 268 // A wrapping container's own children must size from their content,
@@ -189,8 +278,9 @@
189 278 // Derived from the pairs rather than stored, so it costs nothing in
190 279 // the bucket tree, and — because this runs after style_hash() has
191 280 // read $rules — the hash in already-saved markup is unaffected.
192 281 $body .= self::wrap_child_css( $pairs, $selector_base . $state );
282 + $body .= self::flex_child_css( $pairs, $selector_base . $state );
193 283
194 284 $css .= ( '' !== $media ) ? $media . '{' . $body . '}' : $body;
195 285 }
196 286 return $css;
@@ -196,8 +286,82 @@
196 286 return $css;
197 287 }
198 288
199 289 /**
290 + * The value a declaration is emitted with, which is its compiled value
291 + * except for the centring guard's auto margins (see state_declarations()).
292 + *
293 + * Those centre a width-capped box in normal flow, but in a flex row an
294 + * auto margin swallows the free space on the main axis and overrides the
295 + * parent's justify-content: two 180px children of a centred, 24px-gap row
296 + * came out ~116px apart, the leftover space split into all four margins.
297 + * Emitted through a custom property instead, which the parent resolves
298 + * for its own children (flex_child_css()) and which falls back to the
299 + * same `auto` everywhere else. An `auto` here can only be the guard's: the
300 + * Dimensions control always appends a unit, and the guard stands down
301 + * when the author set a horizontal margin.
302 + *
303 + * Rewritten at emission, after style_hash() has read the pairs, so the
304 + * class in already-saved markup is unaffected.
305 + *
306 + * Mirrors emittedValue() in atomic-shared/styles.js.
307 + *
308 + * @param string $property The CSS property.
309 + * @param string $value The compiled value.
310 + * @return string The value to emit.
311 + */
312 + public static function emitted_value( $property, $value ) {
313 + if ( 'auto' === $value && ( 'margin-left' === $property || 'margin-right' === $property ) ) {
314 + return 'var(--ablocks-center-margin,auto)';
315 + }
316 + return $value;
317 + }
318 +
319 + /**
320 + * The child rule a bucket that sets `display` or `flex-direction` needs,
321 + * or ''. Resolves --ablocks-center-margin (see emitted_value()) for this
322 + * container's own children: 0 across a flex row's main axis, the `auto`
323 + * fallback in a flex column (where horizontal is the cross axis and
324 + * centring cannot open a gap) and in any other display.
325 + *
326 + * Display and direction are separate variables because they may come
327 + * from different breakpoints — a row at Desktop turned into a column at
328 + * Tablet only compiles `flex-direction` there — and the cascade has to
329 + * combine them. The inner var() is substituted on the child itself, so
330 + * it reads the direction set for that same child.
331 + *
332 + * Custom properties inherit, so the grandchildren are reset to the
333 + * guaranteed-invalid value; `:where()` keeps that at 0-0-0, below any
334 + * nested container's own child rule. Grid is left alone on purpose: an
335 + * auto margin there centres an item in its own cell and opens no gap.
336 + *
337 + * Mirrors flexChildCss() in atomic-shared/styles.js.
338 + *
339 + * @param array $pairs The bucket's declaration pairs.
340 + * @param string $selector The already-composed selector for this bucket.
341 + * @return string A CSS rule, or ''.
342 + */
343 + public static function flex_child_css( $pairs, $selector ) {
344 + $declarations = '';
345 + foreach ( $pairs as $pair ) {
346 + if ( 'display' === $pair[0] ) {
347 + $declarations .= ( 'flex' === $pair[1] || 'inline-flex' === $pair[1] )
348 + ? '--ablocks-center-margin:var(--ablocks-column-margin,0);'
349 + : '--ablocks-center-margin:initial;';
350 + } elseif ( 'flex-direction' === $pair[0] ) {
351 + $declarations .= ( 'column' === $pair[1] || 'column-reverse' === $pair[1] )
352 + ? '--ablocks-column-margin:auto;'
353 + : '--ablocks-column-margin:initial;';
354 + }
355 + }
356 + if ( '' === $declarations ) {
357 + return '';
358 + }
359 + return $selector . '>*{' . $declarations . '}'
360 + . ':where(' . $selector . '>*>*){--ablocks-center-margin:initial;--ablocks-column-margin:initial;}';
361 + }
362 +
363 + /**
200 364 * The child rule a wrapping container needs, or ''.
201 365 *
202 366 * Scoped to container children only, matching the base stylesheets — a leaf
203 367 * block is sized by its own block, not by the row it sits in. `:where()`
@@ -278,8 +442,20 @@
278 442 && '' === StyleBuckets::device_bucket_key( $bucket['device'] );
279 443 }
280 444
281 445 /**
446 + * Whether a backgroundColor value is itself a gradient function (the
447 + * Background tab's Color control can now produce one — see
448 + * ABlocksColorControl's `isGradient` picker). Such a value is not valid
449 + * CSS under `background-color`; it belongs under `background-image`
450 + * instead, the same property the legacy `backgroundGradient` scalar and
451 + * `backgroundOverlay` layers already use. Mirrors JS `isGradientValue()`.
452 + */
453 + public static function is_gradient_value( $value ) {
454 + return is_string( $value ) && ( 0 === strpos( $value, 'linear-gradient(' ) || 0 === strpos( $value, 'radial-gradient(' ) );
455 + }
456 +
457 + /**
282 458 * A bucket that sets a solid background colour and no gradient of its own
283 459 * must clear any gradient inherited from a lower-precedence bucket:
284 460 * `background-color` and `background-image` are separate properties, so the
285 461 * gradient would otherwise stay painted on top of the solid colour.
@@ -312,9 +488,9 @@
312 488 * rather than enumerating props itself, so the JS editor compiler and this
313 489 * one cannot drift on which props exist, what CSS property each maps to, or
314 490 * what order they emit in.
315 491 */
316 - public static function state_declarations( $props ) {
492 + public static function state_declarations( $props, $inherited_border_style = '' ) {
317 493 $css = [];
318 494 if ( ! is_array( $props ) ) {
319 495 return $css;
320 496 }
@@ -335,9 +511,17 @@
335 511
336 512 case 'color':
337 513 $value = self::read_scalar( $props, $prop );
338 514 if ( '' !== $value ) {
339 - $css[ $entry['css'] ] = Color::get_css( $value );
515 + $css_value = Color::get_css( $value );
516 + // backgroundColor is the one colour prop whose value can be
517 + // a gradient function; every other colour prop (textColor,
518 + // borderColor) keeps writing its own CSS property as before.
519 + if ( 'backgroundColor' === $prop && self::is_gradient_value( $css_value ) ) {
520 + $css['background-image'] = $css_value;
521 + } else {
522 + $css[ $entry['css'] ] = $css_value;
523 + }
340 524 }
341 525 break;
342 526
343 527 case 'scalar':
@@ -377,9 +561,9 @@
377 561 }
378 562 break;
379 563
380 564 case 'border':
381 - $css = array_merge( $css, self::border_css( $props ) );
565 + $css = array_merge( $css, self::border_css( $props, $inherited_border_style ) );
382 566 break;
383 567
384 568 case 'dimensions':
385 569 $css = array_merge( $css, self::spacing_css( $props, $prop ) );
@@ -470,9 +654,9 @@
470 654 * OR only a colour still gets one. Colour-only is the common case — a hover
471 655 * bucket that recolours an existing border — and it rendered nothing before
472 656 * this fallback covered it.
473 657 */
474 - private static function border_css( $props ) {
658 + private static function border_css( $props, $inherited_style = '' ) {
475 659 $css = [];
476 660
477 661 $width = self::read_range( $props, 'borderWidth' );
478 662 $style = self::read_scalar( $props, 'borderStyle' );
@@ -501,9 +685,18 @@
501 685 }
502 686
503 687 // A width on any single side needs a style too, or it paints nothing.
504 688 if ( '' !== $width || $has_side_width || '' !== $color ) {
505 - $css['border-style'] = '' !== $style ? $style : 'solid';
689 + // Without a type of its own the bucket carries the one it inherits:
690 + // this rule's border-style would otherwise override a wider
691 + // device's (or the normal state's) `dashed` with a `solid` nobody
692 + // chose. `solid` is only the default when nothing up the cascade
693 + // sets a type either.
694 + if ( '' !== $style ) {
695 + $css['border-style'] = $style;
696 + } else {
697 + $css['border-style'] = '' !== $inherited_style ? $inherited_style : 'solid';
698 + }
506 699 } elseif ( '' !== $style ) {
507 700 // A style on its own is meaningful (e.g. `none` to remove a border).
508 701 $css['border-style'] = $style;
509 702 }