| 1 |
<?php |
| 2 |
namespace ABlocks\Classes; |
| 3 |
|
| 4 |
if ( ! defined( 'ABSPATH' ) ) { |
| 5 |
exit; |
| 6 |
} |
| 7 |
|
| 8 |
/** |
| 9 |
* The single prop -> CSS descriptor for the atomic style system. |
| 10 |
* |
| 11 |
* Both compilers read this instead of keeping their own prop lists: the PHP |
| 12 |
* frontend compiler (AtomicStyles) and the JS editor compiler (the mirror in |
| 13 |
* src/blocks/atomic-shared/styles-schema.js). Adding a style prop means one |
| 14 |
* entry here and one in the mirror — nothing else enumerates props. |
| 15 |
* |
| 16 |
* The array order IS the canonical emission order. Declarations are emitted in |
| 17 |
* this order on both sides so the two compilers serialise identically, which is |
| 18 |
* what lets a block's style hash be computed in the editor and matched on the |
| 19 |
* front end. |
| 20 |
* |
| 21 |
* Each entry is [ prop, css, kind ] where kind is one of: |
| 22 |
* typography - the `typography` object, compiled by Controls\Typography |
| 23 |
* color - a scalar resolved through Controls\Color (supports globals) |
| 24 |
* scalar - a scalar emitted as-is |
| 25 |
* range - an aBlocks Range object ({ value, valueUnit }) -> "<value><unit>" |
| 26 |
* border - the border group (width/style/colour/radius + its fallbacks) |
| 27 |
* dimensions - padding / margin, compiled by Controls\Dimensions |
| 28 |
* effect - a repeatable list, compiled by StyleEffects |
| 29 |
* overlay - background overlay layers, compiled by StyleBackground |
| 30 |
* clip - background-clip plus its -webkit- companion |
| 31 |
*/ |
| 32 |
class StylesSchema { |
| 33 |
|
| 34 |
/** |
| 35 |
* The border group's members, in the order they are emitted. Declared here |
| 36 |
* rather than inline in the compiler so the JS mirror can assert the same |
| 37 |
* set. |
| 38 |
* |
| 39 |
* The per-side widths and per-corner radii are optional overrides on top of |
| 40 |
* the uniform value — a block that sets none of them compiles exactly as it |
| 41 |
* did before they existed, which matters because the style class is a hash |
| 42 |
* of compiled output. |
| 43 |
*/ |
| 44 |
const BORDER_SIDES = [ 'Top', 'Right', 'Bottom', 'Left' ]; |
| 45 |
|
| 46 |
/** Corner prop suffix -> CSS property, in CSS shorthand order. */ |
| 47 |
const BORDER_CORNERS = [ |
| 48 |
'TopLeft' => 'border-top-left-radius', |
| 49 |
'TopRight' => 'border-top-right-radius', |
| 50 |
'BottomRight' => 'border-bottom-right-radius', |
| 51 |
'BottomLeft' => 'border-bottom-left-radius', |
| 52 |
]; |
| 53 |
|
| 54 |
const BORDER_MEMBERS = [ |
| 55 |
'borderWidth', |
| 56 |
'borderWidthTop', |
| 57 |
'borderWidthRight', |
| 58 |
'borderWidthBottom', |
| 59 |
'borderWidthLeft', |
| 60 |
'borderStyle', |
| 61 |
'borderColor', |
| 62 |
'borderRadius', |
| 63 |
'borderRadiusTopLeft', |
| 64 |
'borderRadiusTopRight', |
| 65 |
'borderRadiusBottomRight', |
| 66 |
'borderRadiusBottomLeft', |
| 67 |
]; |
| 68 |
|
| 69 |
/** Ordered descriptor. */ |
| 70 |
public static function props() { |
| 71 |
return [ |
| 72 |
[ 'prop' => 'typography', 'css' => '', 'kind' => 'typography' ], |
| 73 |
|
| 74 |
[ 'prop' => 'textColor', 'css' => 'color', 'kind' => 'color' ], |
| 75 |
[ 'prop' => 'backgroundColor', 'css' => 'background-color', 'kind' => 'color' ], |
| 76 |
[ 'prop' => 'backgroundGradient', 'css' => 'background-image', 'kind' => 'scalar' ], |
| 77 |
// Overlay layers expand to background-image + the four positional |
| 78 |
// props at once, so they emit as a group (like border / dimensions). |
| 79 |
[ 'prop' => 'backgroundOverlay', 'css' => '', 'kind' => 'overlay' ], |
| 80 |
|
| 81 |
[ 'prop' => 'border', 'css' => '', 'kind' => 'border' ], |
| 82 |
|
| 83 |
// Size. |
| 84 |
[ 'prop' => 'width', 'css' => 'width', 'kind' => 'range' ], |
| 85 |
[ 'prop' => 'maxWidth', 'css' => 'max-width', 'kind' => 'range' ], |
| 86 |
[ 'prop' => 'minWidth', 'css' => 'min-width', 'kind' => 'range' ], |
| 87 |
[ 'prop' => 'height', 'css' => 'height', 'kind' => 'range' ], |
| 88 |
[ 'prop' => 'minHeight', 'css' => 'min-height', 'kind' => 'range' ], |
| 89 |
[ 'prop' => 'maxHeight', 'css' => 'max-height', 'kind' => 'range' ], |
| 90 |
[ 'prop' => 'aspectRatio', 'css' => 'aspect-ratio', 'kind' => 'scalar' ], |
| 91 |
|
| 92 |
// Gaps. |
| 93 |
[ 'prop' => 'gap', 'css' => 'gap', 'kind' => 'range' ], |
| 94 |
[ 'prop' => 'rowGap', 'css' => 'row-gap', 'kind' => 'range' ], |
| 95 |
[ 'prop' => 'columnGap', 'css' => 'column-gap', 'kind' => 'range' ], |
| 96 |
[ 'prop' => 'strokeWidth', 'css' => 'stroke-width', 'kind' => 'range' ], |
| 97 |
|
| 98 |
// Position offsets. |
| 99 |
[ 'prop' => 'top', 'css' => 'top', 'kind' => 'range' ], |
| 100 |
[ 'prop' => 'right', 'css' => 'right', 'kind' => 'range' ], |
| 101 |
[ 'prop' => 'bottom', 'css' => 'bottom', 'kind' => 'range' ], |
| 102 |
[ 'prop' => 'left', 'css' => 'left', 'kind' => 'range' ], |
| 103 |
// Elementor's "Anchor offset": how far above the element an in-page |
| 104 |
// anchor link should stop, so a fixed header does not cover it. |
| 105 |
[ 'prop' => 'anchorOffset', 'css' => 'scroll-margin-top', 'kind' => 'range' ], |
| 106 |
|
| 107 |
// Layout. |
| 108 |
[ 'prop' => 'display', 'css' => 'display', 'kind' => 'scalar' ], |
| 109 |
[ 'prop' => 'flexDirection', 'css' => 'flex-direction', 'kind' => 'scalar' ], |
| 110 |
[ 'prop' => 'flexWrap', 'css' => 'flex-wrap', 'kind' => 'scalar' ], |
| 111 |
[ 'prop' => 'justifyContent', 'css' => 'justify-content', 'kind' => 'scalar' ], |
| 112 |
[ 'prop' => 'alignItems', 'css' => 'align-items', 'kind' => 'scalar' ], |
| 113 |
[ 'prop' => 'alignContent', 'css' => 'align-content', 'kind' => 'scalar' ], |
| 114 |
[ 'prop' => 'gridTemplateColumns', 'css' => 'grid-template-columns', 'kind' => 'scalar' ], |
| 115 |
[ 'prop' => 'gridTemplateRows', 'css' => 'grid-template-rows', 'kind' => 'scalar' ], |
| 116 |
[ 'prop' => 'gridAutoFlow', 'css' => 'grid-auto-flow', 'kind' => 'scalar' ], |
| 117 |
[ 'prop' => 'gridAutoColumns', 'css' => 'grid-auto-columns', 'kind' => 'scalar' ], |
| 118 |
[ 'prop' => 'gridAutoRows', 'css' => 'grid-auto-rows', 'kind' => 'scalar' ], |
| 119 |
|
| 120 |
// Flex-item behaviour (when placed inside an atomic flex/grid parent). |
| 121 |
[ 'prop' => 'flexBasis', 'css' => 'flex-basis', 'kind' => 'scalar' ], |
| 122 |
[ 'prop' => 'flexGrow', 'css' => 'flex-grow', 'kind' => 'scalar' ], |
| 123 |
[ 'prop' => 'flexShrink', 'css' => 'flex-shrink', 'kind' => 'scalar' ], |
| 124 |
[ 'prop' => 'order', 'css' => 'order', 'kind' => 'scalar' ], |
| 125 |
[ 'prop' => 'alignSelf', 'css' => 'align-self', 'kind' => 'scalar' ], |
| 126 |
|
| 127 |
// Media (image / svg). |
| 128 |
[ 'prop' => 'objectFit', 'css' => 'object-fit', 'kind' => 'scalar' ], |
| 129 |
[ 'prop' => 'objectPosition', 'css' => 'object-position', 'kind' => 'scalar' ], |
| 130 |
[ 'prop' => 'overflow', 'css' => 'overflow', 'kind' => 'scalar' ], |
| 131 |
[ 'prop' => 'fill', 'css' => 'fill', 'kind' => 'scalar' ], |
| 132 |
[ 'prop' => 'stroke', 'css' => 'stroke', 'kind' => 'scalar' ], |
| 133 |
|
| 134 |
[ 'prop' => 'textAlign', 'css' => 'text-align', 'kind' => 'scalar' ], |
| 135 |
|
| 136 |
// Position. |
| 137 |
[ 'prop' => 'position', 'css' => 'position', 'kind' => 'scalar' ], |
| 138 |
[ 'prop' => 'zIndex', 'css' => 'z-index', 'kind' => 'scalar' ], |
| 139 |
|
| 140 |
// Effects. |
| 141 |
[ 'prop' => 'transform', 'css' => 'transform', 'kind' => 'effect' ], |
| 142 |
[ 'prop' => 'transformOrigin', 'css' => 'transform-origin', 'kind' => 'scalar' ], |
| 143 |
[ 'prop' => 'filter', 'css' => 'filter', 'kind' => 'effect' ], |
| 144 |
[ 'prop' => 'backdropFilter', 'css' => 'backdrop-filter', 'kind' => 'effect' ], |
| 145 |
[ 'prop' => 'mixBlendMode', 'css' => 'mix-blend-mode', 'kind' => 'scalar' ], |
| 146 |
// `isolation: isolate` starts a stacking context, which is what |
| 147 |
// confines a CHILD's blend mode to this element instead of letting it |
| 148 |
// blend with whatever the page happens to paint behind it. |
| 149 |
[ 'prop' => 'isolation', 'css' => 'isolation', 'kind' => 'scalar' ], |
| 150 |
[ 'prop' => 'opacity', 'css' => 'opacity', 'kind' => 'scalar' ], |
| 151 |
[ 'prop' => 'boxShadow', 'css' => 'box-shadow', 'kind' => 'effect' ], |
| 152 |
[ 'prop' => 'textShadow', 'css' => 'text-shadow', 'kind' => 'effect' ], |
| 153 |
[ 'prop' => 'cursor', 'css' => 'cursor', 'kind' => 'scalar' ], |
| 154 |
[ 'prop' => 'transition', 'css' => 'transition', 'kind' => 'effect' ], |
| 155 |
|
| 156 |
// Background (image layer settings; the colour/gradient are above). |
| 157 |
[ 'prop' => 'backgroundRepeat', 'css' => 'background-repeat', 'kind' => 'scalar' ], |
| 158 |
[ 'prop' => 'backgroundSize', 'css' => 'background-size', 'kind' => 'scalar' ], |
| 159 |
[ 'prop' => 'backgroundPosition', 'css' => 'background-position', 'kind' => 'scalar' ], |
| 160 |
[ 'prop' => 'backgroundAttachment', 'css' => 'background-attachment', 'kind' => 'scalar' ], |
| 161 |
// `background-clip: text` needs the -webkit- prefix to paint anywhere |
| 162 |
// but the newest engines, so this kind emits the pair. |
| 163 |
[ 'prop' => 'backgroundClip', 'css' => 'background-clip', 'kind' => 'clip' ], |
| 164 |
|
| 165 |
// Spacing. |
| 166 |
[ 'prop' => 'padding', 'css' => 'padding', 'kind' => 'dimensions' ], |
| 167 |
[ 'prop' => 'margin', 'css' => 'margin', 'kind' => 'dimensions' ], |
| 168 |
]; |
| 169 |
} |
| 170 |
|
| 171 |
/** |
| 172 |
* Reorder a declarations map into canonical order, so both compilers |
| 173 |
* serialise the same declaration set identically — which is what makes the |
| 174 |
* style hash reproducible across the editor and the front end. |
| 175 |
* |
| 176 |
* Some descriptor entries emit CSS properties the descriptor does not name |
| 177 |
* one-by-one: `typography` expands to font-family/font-size/…, `dimensions` |
| 178 |
* to padding-top/padding-right/…, and the border group to its longhands. An |
| 179 |
* unnamed property therefore inherits the rank of the last named property |
| 180 |
* before it, keeping it adjacent to the group that produced it. Compiler |
| 181 |
* output is already in descriptor order, so this is a stable no-op there and |
| 182 |
* a normaliser for maps assembled anywhere else. |
| 183 |
*/ |
| 184 |
/** |
| 185 |
* @param array $declarations CSS declarations keyed by property. |
| 186 |
* @return array The same declarations in canonical order. |
| 187 |
*/ |
| 188 |
public static function canonical_order( $declarations ) { |
| 189 |
if ( ! is_array( $declarations ) || empty( $declarations ) ) { |
| 190 |
return $declarations; |
| 191 |
} |
| 192 |
|
| 193 |
$rank = []; |
| 194 |
foreach ( self::props() as $i => $entry ) { |
| 195 |
if ( '' !== $entry['css'] ) { |
| 196 |
$rank[ $entry['css'] ] = $i; |
| 197 |
} |
| 198 |
} |
| 199 |
|
| 200 |
$indexed = []; |
| 201 |
$n = 0; |
| 202 |
$last_rank = 0; |
| 203 |
foreach ( $declarations as $property => $value ) { |
| 204 |
if ( isset( $rank[ $property ] ) ) { |
| 205 |
$last_rank = $rank[ $property ]; |
| 206 |
} |
| 207 |
$indexed[] = [ $last_rank, $n++, $property, $value ]; |
| 208 |
} |
| 209 |
|
| 210 |
usort( |
| 211 |
$indexed, |
| 212 |
function ( $a, $b ) { |
| 213 |
return $a[0] === $b[0] ? $a[1] - $b[1] : $a[0] - $b[0]; |
| 214 |
} |
| 215 |
); |
| 216 |
|
| 217 |
$out = []; |
| 218 |
foreach ( $indexed as $row ) { |
| 219 |
$out[ $row[2] ] = $row[3]; |
| 220 |
} |
| 221 |
return $out; |
| 222 |
} |
| 223 |
} |
| 224 |
|