PluginProbe
Gutenify – Visual Site Builder Blocks & Starter Templates / trunk
Gutenify – Visual Site Builder Blocks & Starter Templates vtrunk
1.9.0 1.8.0 1.7.0 1.6.6 1.6.5 1.6.4 trunk 0.0.1 0.0.2 0.0.3 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 All 74 releases
gutenify / core / dist / non-blocks / extend / core-button / index.php

index.php in Gutenify – Visual Site Builder Blocks & Starter Templates trunk, at core/dist/non-blocks/extend/core-button/index.php

325 lines 11.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace gutenify;
3
4 defined( 'ABSPATH' ) || exit;
5
6 /**
7 * Extends core/button with an icon and hover effects (motion, background).
8 * Hover colors and tablet/mobile styling come from core's block states (WP 7.1+).
9 *
10 * The effects live in one static stylesheet (styles/frontend.scss) shared with
11 * the editor. This class only adds the classes and CSS variables it reads, and
12 * mirrors fx.js, so keep get_fx() in sync with it. The allowed values come from
13 * presets.json, which both sides read.
14 */
15 class Core_Button {
16 /**
17 * Icon hashes already written to the stylesheet during this request.
18 *
19 * @var string[]
20 */
21 private static $emitted_icons = array();
22
23 public static function init() {
24 add_action( 'init', array( __CLASS__, 'register_style' ) );
25 add_action( 'enqueue_block_assets', array( __CLASS__, 'enqueue_editor_style' ) );
26 add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'register_editor_script' ) );
27 add_filter( 'gutenify_render_block_core/button', array( __CLASS__, 'render' ), 10, 4 );
28
29 // The legacy Gutenify buttons are replaced by core's once core has block states.
30 add_filter( 'register_block_type_args', array( __CLASS__, 'hide_legacy_blocks' ), 10, 2 );
31 $constants = Helpers::plugin_constants();
32 add_filter( $constants['plugin_main_slug'] . '--editor--localized-vars', array( __CLASS__, 'add_editor_flags' ) );
33 }
34
35 /**
36 * Whether core (WP 7.1+) or the Gutenberg plugin has per-block states, which
37 * is what covers the legacy block's hover and tablet/mobile options.
38 *
39 * @return bool
40 */
41 public static function core_has_block_states() {
42 return function_exists( 'wp_normalize_state_preset_vars' ) || function_exists( 'gutenberg_normalize_state_preset_vars' );
43 }
44
45 /**
46 * Hides the legacy blocks from the inserter, but keeps them registered so
47 * saved content still renders and edits.
48 *
49 * @param array $args Block type arguments.
50 * @param string $block_name Block name.
51 * @return array
52 */
53 public static function hide_legacy_blocks( $args, $block_name ) {
54 if ( in_array( $block_name, array( 'gutenify/button', 'gutenify/buttons' ), true ) && self::core_has_block_states() ) {
55 $args['supports'] = isset( $args['supports'] ) && is_array( $args['supports'] ) ? $args['supports'] : array();
56 $args['supports']['inserter'] = false;
57 }
58 return $args;
59 }
60
61 /**
62 * Tells the editor script about the above, so it hides the same blocks.
63 *
64 * @param array $vars Editor variables.
65 * @return array
66 */
67 public static function add_editor_flags( $vars ) {
68 $vars['core_block_states'] = self::core_has_block_states();
69 return $vars;
70 }
71
72 private static function handle() {
73 $constants = Helpers::plugin_constants();
74 return $constants['plugin_main_slug'] . '-extend-core-button';
75 }
76
77 public static function register_style() {
78 $base_dir = Helpers::core_base_dir();
79 $base_url = Helpers::core_base_url();
80 $assets = Helpers::asset_file_values( sprintf( '%sdist/non-blocks/extend/core-button/frontend.asset.php', $base_dir ) );
81
82 wp_register_style( self::handle(), $base_url . 'dist/non-blocks/extend/core-button/frontend.css', array(), $assets['version'] );
83 }
84
85 /**
86 * The editor canvas loads the same stylesheet as the frontend. On the
87 * frontend it is only enqueued when a button uses an effect or icon.
88 */
89 public static function enqueue_editor_style() {
90 if ( is_admin() ) {
91 wp_enqueue_style( self::handle() );
92 }
93 }
94
95 public static function register_editor_script() {
96 $base_dir = Helpers::core_base_dir();
97 $base_url = Helpers::core_base_url();
98 $assets = Helpers::asset_file_values( sprintf( '%sdist/non-blocks/extend/core-button/index.asset.php', $base_dir ) );
99 $handle = self::handle() . '-editor';
100
101 wp_register_script( $handle, $base_url . 'dist/non-blocks/extend/core-button/index.js', $assets['dependencies'], $assets['version'], true );
102 wp_enqueue_script( $handle );
103
104 wp_register_style( $handle, $base_url . 'dist/non-blocks/extend/core-button/index.css', array(), $assets['version'] );
105 wp_enqueue_style( $handle );
106 }
107
108 private static function presets() {
109 static $presets = null;
110 if ( null === $presets ) {
111 $presets = wp_json_file_decode( __DIR__ . '/presets.json', array( 'associative' => true ) );
112 if ( ! is_array( $presets ) ) {
113 // Fail closed: no effects rather than a fatal error.
114 $presets = array(
115 'defaults' => array(
116 'duration' => 250,
117 'tintStrength' => 20,
118 'easing' => 'ease',
119 ),
120 'limits' => array(
121 'duration' => array( 50, 1500 ),
122 'tintStrength' => array( 5, 40 ),
123 'iconSvgMax' => 20000,
124 ),
125 'motion' => array(),
126 'background' => array(),
127 'nudge' => array(),
128 'easing' => array( 'ease' => 'ease' ),
129 );
130 }
131 }
132 return $presets;
133 }
134
135 /**
136 * Allows only a plain length such as `1.4em` or `12px`. Anything else is
137 * dropped, so a value can never carry extra CSS (url(), unbalanced
138 * brackets that swallow the next variable, ...).
139 *
140 * @param mixed $value Raw attribute value.
141 * @return string The value, or an empty string.
142 */
143 private static function css_length( $value ) {
144 $value = is_string( $value ) || is_numeric( $value ) ? trim( (string) $value ) : '';
145 return preg_match( '/^-?(?:\d+\.?\d*|\.\d+)(?:px|em|rem|%|vw|vh|vmin|vmax|ch|ex)?$/iD', $value ) ? $value : '';
146 }
147
148 /**
149 * Allows only a plain color: hex, rgb()/hsl() with numbers, a theme preset
150 * variable, or a color keyword. Anything else is dropped.
151 *
152 * @param mixed $value Raw attribute value.
153 * @return string The value, or an empty string.
154 */
155 private static function css_color( $value ) {
156 $value = is_string( $value ) ? trim( $value ) : '';
157 return preg_match( '/^(?:#[0-9a-f]{3,8}|(?:rgb|hsl)a?\([\d\s.,%\/deg]+\)|var\(--wp--preset--[a-z0-9-]+--[a-z0-9-]+\)|[a-z]{3,20})$/iD', $value ) ? $value : '';
158 }
159
160 /**
161 * Turns core's `var:preset|color|slug` form into a CSS variable reference.
162 *
163 * @param mixed $value Style value.
164 * @return mixed
165 */
166 private static function preset_to_var( $value ) {
167 if ( is_string( $value ) && 0 === strpos( $value, 'var:preset|' ) ) {
168 return 'var(--wp--' . str_replace( '|', '--', substr( $value, 4 ) ) . ')';
169 }
170 return $value;
171 }
172
173 private static function clamp( $value, $limits, $fallback ) {
174 $number = is_numeric( $value ) ? (int) $value : 0;
175 return $number > 0 ? min( $limits[1], max( $limits[0], $number ) ) : $fallback;
176 }
177
178 private static function key_in( $value, $allowed ) {
179 return is_string( $value ) && in_array( $value, $allowed, true ) ? $value : '';
180 }
181
182 /**
183 * Turns the block attributes into wrapper classes and CSS variables.
184 *
185 * @param array $attrs Block attributes.
186 * @return array{classes: string[], vars: array<string,string>, svg: string}
187 */
188 public static function get_fx( $attrs ) {
189 $presets = self::presets();
190 $icon = isset( $attrs['gtfyIcon'] ) && is_array( $attrs['gtfyIcon'] ) ? $attrs['gtfyIcon'] : array();
191 $hover = isset( $attrs['gtfyHoverEffect'] ) && is_array( $attrs['gtfyHoverEffect'] ) ? $attrs['gtfyHoverEffect'] : array();
192 $classes = array();
193 $vars = array();
194 $svg = '';
195 $raw = isset( $icon['svg'] ) && is_string( $icon['svg'] ) ? trim( $icon['svg'] ) : '';
196 $has_icon = '' !== $raw && 0 === stripos( $raw, '<svg' ) && strlen( $raw ) <= $presets['limits']['iconSvgMax'];
197
198 if ( $has_icon ) {
199 $svg = $raw;
200 $classes[] = 'gtfy-icon';
201 $classes[] = ( isset( $icon['position'] ) && 'after' === $icon['position'] ) ? 'gtfy-icon-after' : 'gtfy-icon-before';
202 if ( ! empty( $icon['hideText'] ) ) {
203 $classes[] = 'gtfy-icon-only';
204 }
205 $size = self::css_length( $icon['size'] ?? '' );
206 $gap = self::css_length( $icon['gap'] ?? '' );
207 if ( '' !== $size ) {
208 $vars['--gtfy-icon-size'] = $size;
209 }
210 if ( '' !== $gap ) {
211 $vars['--gtfy-icon-gap'] = $gap;
212 }
213 }
214
215 $motion = self::key_in( $hover['motion'] ?? null, $presets['motion'] );
216 $animate = ! empty( $hover['animate'] );
217 // An icon-only button has nothing else to show, so revealing the icon on hover makes no sense.
218 $reveal = $has_icon && empty( $icon['hideText'] ) && ! empty( $hover['iconReveal'] );
219 $nudge = $has_icon ? self::key_in( $hover['iconNudge'] ?? null, $presets['nudge'] ) : '';
220 $background = self::key_in( $hover['background'] ?? null, $presets['background'] );
221
222 if ( 0 === strpos( $background, 'sweep' ) ) {
223 // A gradient hover is emitted by core as a `background` shorthand with !important, which resets the size and base color, so it can't be swept.
224 $colors = isset( $attrs['style'][':hover']['color'] ) && is_array( $attrs['style'][':hover']['color'] ) ? $attrs['style'][':hover']['color'] : array();
225 $fill = self::css_color( self::preset_to_var( $colors['background'] ?? '' ) );
226 if ( '' !== $fill && empty( $colors['gradient'] ) ) {
227 $vars['--gtfy-fill'] = $fill;
228 $classes[] = 'gtfy-fx-sweep';
229 } else {
230 $background = '';
231 }
232 }
233
234 if ( 'darken' === $background || 'lighten' === $background ) {
235 $strength = self::clamp( $hover['tintStrength'] ?? 0, $presets['limits']['tintStrength'], $presets['defaults']['tintStrength'] );
236 $vars['--gtfy-tint'] = rtrim( rtrim( number_format( $strength / 100, 2, '.', '' ), '0' ), '.' );
237 }
238
239 if ( '' !== $motion ) {
240 $classes[] = 'gtfy-motion-' . $motion;
241 }
242 if ( '' !== $background ) {
243 $classes[] = 'gtfy-bg-' . $background;
244 }
245 if ( '' !== $nudge ) {
246 $classes[] = 'gtfy-nudge';
247 $classes[] = 'gtfy-nudge-' . $nudge;
248 }
249 if ( $reveal ) {
250 $classes[] = 'gtfy-icon-reveal';
251 }
252 if ( ! empty( $hover['activePress'] ) ) {
253 $classes[] = 'gtfy-active-press';
254 }
255
256 if ( '' !== $motion || '' !== $background || $animate || '' !== $nudge || $reveal ) {
257 $classes[] = 'gtfy-fx';
258 if ( '' !== $motion || '' !== $background || $animate ) {
259 $classes[] = 'gtfy-fx-transition';
260 }
261 if ( '' !== $nudge || $reveal ) {
262 $classes[] = 'gtfy-fx-icon';
263 }
264 if ( isset( $hover['animateOut'] ) && false === $hover['animateOut'] ) {
265 $classes[] = 'gtfy-fx-instant';
266 }
267 $easing = isset( $hover['easing'] ) && is_string( $hover['easing'] ) && isset( $presets['easing'][ $hover['easing'] ] ) ? $hover['easing'] : $presets['defaults']['easing'];
268 $vars['--gtfy-duration'] = self::clamp( $hover['duration'] ?? 0, $presets['limits']['duration'], $presets['defaults']['duration'] ) . 'ms';
269 $vars['--gtfy-easing'] = $presets['easing'][ $easing ];
270 }
271
272 return array(
273 'classes' => $classes,
274 'vars' => $vars,
275 'svg' => $svg,
276 );
277 }
278
279 public static function render( $content, $block, $instance, $block_id ) {
280 if ( empty( $block['blockName'] ) || 'core/button' !== $block['blockName'] ) {
281 return $content;
282 }
283
284 $fx = self::get_fx( $block['attrs'] ?? array() );
285 if ( empty( $fx['classes'] ) ) {
286 return $content;
287 }
288
289 $processor = new \WP_HTML_Tag_Processor( $content );
290 if ( ! $processor->next_tag() ) {
291 return $content;
292 }
293
294 foreach ( $fx['classes'] as $class_name ) {
295 $processor->add_class( $class_name );
296 }
297
298 $declarations = '';
299 foreach ( $fx['vars'] as $name => $value ) {
300 $declarations .= $name . ':' . $value . ';';
301 }
302 if ( '' !== $declarations ) {
303 $existing = $processor->get_attribute( 'style' );
304 $existing = is_string( $existing ) && '' !== trim( $existing ) ? rtrim( trim( $existing ), ';' ) . ';' : '';
305 $processor->set_attribute( 'style', $existing . $declarations );
306 }
307
308 if ( '' !== $fx['svg'] ) {
309 // One rule per distinct icon, however many buttons use it.
310 $hash = substr( md5( $fx['svg'] ), 0, 8 );
311 $processor->add_class( 'gtfy-icon-' . $hash );
312 if ( ! in_array( $hash, self::$emitted_icons, true ) ) {
313 self::$emitted_icons[] = $hash;
314 wp_add_inline_style( self::handle(), '.gtfy-icon-' . $hash . '{--gtfy-icon:url("data:image/svg+xml,' . rawurlencode( $fx['svg'] ) . '")}' );
315 }
316 }
317
318 wp_enqueue_style( self::handle() );
319
320 return $processor->get_updated_html();
321 }
322 }
323
324 Core_Button::init();
325