PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks for WordPress Block Editor / 2.0.5
Blockenberg — 600+ Advanced Gutenberg Blocks for WordPress Block Editor v2.0.5
2.0.12 2.0.11 2.0.10 2.0.9 trunk 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8
blockenberg / blockenberg.php

blockenberg.php in Blockenberg — 600+ Advanced Gutenberg Blocks for WordPress Block Editor 2.0.5, at blockenberg.php

921 lines 40.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: Blockenberg
4 * Description: Advanced Gutenberg Blocks for WordPress Block Editor
5 * Version: 2.0.5
6 * Author: Blockenberg
7 * Text Domain: blockenberg
8 * Domain Path: /languages
9 * License: GPLv2 or later
10 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
11 */
12
13 defined( 'ABSPATH' ) || exit;
14
15 /**
16 * Enqueue common editor styles and scripts for all Blockenberg blocks
17 */
18 add_action( 'enqueue_block_editor_assets', function() {
19 wp_enqueue_style( 'dashicons' );
20 // Ensure Media Library modal assets are available for blocks using MediaUpload.
21 wp_enqueue_media();
22
23 // Common editor scripts (branded icons)
24 $editor_js = __DIR__ . '/assets/js/editor.js';
25 if ( file_exists( $editor_js ) ) {
26 wp_enqueue_script(
27 'bkbg-editor-common',
28 plugins_url( 'assets/js/editor.js', __FILE__ ),
29 array( 'wp-blocks', 'wp-dom-ready', 'wp-element' ),
30 filemtime( $editor_js ),
31 true
32 );
33 }
34
35 // Inspector tabs (General / Advanced) for all Blockenberg blocks
36 $inspector_tabs_js = __DIR__ . '/assets/js/inspector-tabs.js';
37 if ( file_exists( $inspector_tabs_js ) ) {
38 wp_enqueue_script(
39 'bkbg-inspector-tabs',
40 plugins_url( 'assets/js/inspector-tabs.js', __FILE__ ),
41 array( 'wp-blocks', 'wp-element', 'wp-compose', 'wp-hooks', 'wp-block-editor', 'wp-components', 'wp-i18n', 'wp-data' ),
42 filemtime( $inspector_tabs_js ),
43 true
44 );
45 }
46 });
47
48 /**
49 * Enqueue editor styles in a way compatible with the iframe-based editor canvas.
50 */
51 add_action( 'enqueue_block_assets', function () {
52 // Avoid loading editor-only CSS on the frontend.
53 if ( ! is_admin() ) {
54 return;
55 }
56
57 // Layout system CSS (shared variables and utilities)
58 $layout_css = __DIR__ . '/assets/css/layout.css';
59 if ( file_exists( $layout_css ) ) {
60 wp_enqueue_style(
61 'bkbg-layout-system',
62 plugins_url( 'assets/css/layout.css', __FILE__ ),
63 array(),
64 filemtime( $layout_css )
65 );
66 }
67
68 // Common editor styles for all blocks
69 $editor_css = __DIR__ . '/assets/css/editor.css';
70 if ( file_exists( $editor_css ) ) {
71 wp_enqueue_style(
72 'bkbg-editor-common',
73 plugins_url( 'assets/css/editor.css', __FILE__ ),
74 array( 'bkbg-layout-system' ),
75 filemtime( $editor_css )
76 );
77 }
78 } );
79
80 /**
81 * Register block scripts with dependencies (but don't enqueue them yet)
82 */
83 add_action( 'init', function () {
84 $blocks_dir = __DIR__ . '/blocks/';
85
86 // Standard WordPress script dependencies for blocks
87 $script_dependencies = array(
88 'wp-blocks',
89 'wp-element',
90 'wp-i18n',
91 'wp-block-editor',
92 'wp-components',
93 'wp-dom-ready',
94 'wp-data'
95 );
96
97 // Standard WordPress style dependencies for blocks
98 $style_dependencies = array(
99 'dashicons'
100 );
101
102 // Register layout system CSS for frontend
103 $layout_css = __DIR__ . '/assets/css/layout.css';
104 if ( file_exists( $layout_css ) ) {
105 wp_register_style(
106 'bkbg-layout-system',
107 plugins_url( 'assets/css/layout.css', __FILE__ ),
108 array(),
109 filemtime( $layout_css )
110 );
111 }
112
113 // Automatically register scripts for all blocks
114 foreach ( glob( $blocks_dir . '*', GLOB_ONLYDIR ) as $block_dir ) {
115 $block_name = basename( $block_dir );
116 $script_file = $block_dir . '/index.js';
117
118 $style_file = $block_dir . '/style.css';
119 $frontend_file = $block_dir . '/frontend.js';
120
121 // Check if block has a JavaScript file
122 if ( file_exists( $script_file ) ) {
123 wp_register_script(
124 'bkbg-' . $block_name . '-editor',
125 plugins_url( 'blocks/' . $block_name . '/index.js', __FILE__ ),
126 $script_dependencies,
127 filemtime( $script_file ),
128 true
129 );
130 }
131
132 // Check if block has a CSS file
133 if ( file_exists( $style_file ) ) {
134 // Layout blocks need the layout system CSS
135 $block_style_deps = $style_dependencies;
136 if ( in_array( $block_name, array( 'section', 'row', 'column' ), true ) ) {
137 $block_style_deps[] = 'bkbg-layout-system';
138 }
139
140 wp_register_style(
141 'bkbg-' . $block_name . '-style',
142 plugins_url( 'blocks/' . $block_name . '/style.css', __FILE__ ),
143 $block_style_deps,
144 filemtime( $style_file )
145 );
146 }
147
148 // Check if block has a frontend JavaScript file
149 if ( file_exists( $frontend_file ) ) {
150 wp_register_script(
151 'bkbg-' . $block_name . '-frontend',
152 plugins_url( 'blocks/' . $block_name . '/frontend.js', __FILE__ ),
153 array(),
154 filemtime( $frontend_file ),
155 true
156 );
157 }
158 }
159
160 // Automatically register all blocks in the /blocks directory
161 foreach ( glob( __DIR__ . '/blocks/*/block.json' ) as $metadata ) {
162 register_block_type( dirname( $metadata ) );
163 }
164 } );
165
166 // Register custom block category and ensure Blockenberg blocks appear in it.
167 add_filter( 'block_categories_all', function( $categories, $block_editor_context ) {
168 // Prepend our custom category so it appears first.
169 array_unshift( $categories, array(
170 'slug' => 'blockenberg',
171 'title' => __( 'Blockenberg Blocks', 'blockenberg' ),
172 'icon' => null,
173 ) );
174 return $categories;
175 }, 10, 2 );
176
177 /**
178 * Register advanced layout attributes on the SERVER side for every Blockenberg block.
179 * Without this, WordPress strips unknown attributes during server-side parsing
180 * (array_intersect_key in WP_Block_Type::prepare_attributes_for_render).
181 */
182 add_filter( 'register_block_type_args', function ( $args, $block_type ) {
183 if ( strpos( $block_type, 'blockenberg/' ) !== 0 ) {
184 return $args;
185 }
186
187 $sides = array( 'Top', 'Right', 'Bottom', 'Left' );
188 $devices = array( '', 'Tablet', 'Mobile' );
189 $extra = array();
190
191 foreach ( array( 'bkbgMargin', 'bkbgPadding' ) as $prefix ) {
192 foreach ( $sides as $side ) {
193 foreach ( $devices as $device ) {
194 $key = $prefix . $side . $device;
195 $extra[ $key ] = array( 'type' => 'string', 'default' => '' );
196 $extra[ $key . 'Unit' ] = array( 'type' => 'string', 'default' => 'px' );
197 }
198 }
199 foreach ( $devices as $device ) {
200 $extra[ $prefix . 'Linked' . $device ] = array( 'type' => 'boolean', 'default' => true );
201 }
202 }
203
204 foreach ( $devices as $device ) {
205 $extra[ 'bkbgZIndex' . $device ] = array( 'type' => 'string', 'default' => '' );
206 }
207
208 $extra['bkbgCssId'] = array( 'type' => 'string', 'default' => '' );
209 $extra['bkbgCssClasses'] = array( 'type' => 'string', 'default' => '' );
210
211 // ── Background attributes ──
212 $extra['bkbgBgType'] = array( 'type' => 'string', 'default' => '' );
213 $extra['bkbgBgHoverType'] = array( 'type' => 'string', 'default' => '' );
214 $extra['bkbgBgColor'] = array( 'type' => 'string', 'default' => '' );
215 $extra['bkbgBgHoverColor']= array( 'type' => 'string', 'default' => '' );
216
217 // Classic image (responsive) — normal & hover
218 foreach ( array( 'bkbgBgImage', 'bkbgBgHoverImage' ) as $img_prefix ) {
219 foreach ( $devices as $device ) {
220 $extra[ $img_prefix . $device ] = array( 'type' => 'string', 'default' => '' );
221 $extra[ $img_prefix . 'Id' . $device ] = array( 'type' => 'number', 'default' => 0 );
222 }
223 }
224
225 // Classic image settings — normal & hover (position/repeat/size responsive, attachment global)
226 foreach ( array( 'bkbgBg', 'bkbgBgHover' ) as $s_prefix ) {
227 foreach ( $devices as $device ) {
228 $extra[ $s_prefix . 'Position' . $device ] = array( 'type' => 'string', 'default' => '' );
229 $extra[ $s_prefix . 'PositionCustomX' . $device ] = array( 'type' => 'string', 'default' => '' );
230 $extra[ $s_prefix . 'PositionCustomY' . $device ] = array( 'type' => 'string', 'default' => '' );
231 $extra[ $s_prefix . 'Repeat' . $device ] = array( 'type' => 'string', 'default' => '' );
232 $extra[ $s_prefix . 'Size' . $device ] = array( 'type' => 'string', 'default' => '' );
233 $extra[ $s_prefix . 'SizeCustomW' . $device ] = array( 'type' => 'string', 'default' => '' );
234 $extra[ $s_prefix . 'SizeCustomH' . $device ] = array( 'type' => 'string', 'default' => '' );
235 }
236 $extra[ $s_prefix . 'Attachment' ] = array( 'type' => 'string', 'default' => '' );
237 }
238
239 // Gradient — normal
240 $extra['bkbgBgGradColor1'] = array( 'type' => 'string', 'default' => '' );
241 $extra['bkbgBgGradColor2'] = array( 'type' => 'string', 'default' => '' );
242 $extra['bkbgBgGradType'] = array( 'type' => 'string', 'default' => 'linear' );
243 foreach ( $devices as $device ) {
244 $extra[ 'bkbgBgGradLoc1' . $device ] = array( 'type' => 'string', 'default' => '' );
245 $extra[ 'bkbgBgGradLoc2' . $device ] = array( 'type' => 'string', 'default' => '' );
246 $extra[ 'bkbgBgGradAngle' . $device ] = array( 'type' => 'string', 'default' => '' );
247 $extra[ 'bkbgBgGradPosition' . $device ] = array( 'type' => 'string', 'default' => '' );
248 }
249
250 // Gradient — hover
251 $extra['bkbgBgHoverGradColor1'] = array( 'type' => 'string', 'default' => '' );
252 $extra['bkbgBgHoverGradColor2'] = array( 'type' => 'string', 'default' => '' );
253 $extra['bkbgBgHoverGradType'] = array( 'type' => 'string', 'default' => 'linear' );
254 foreach ( $devices as $device ) {
255 $extra[ 'bkbgBgHoverGradLoc1' . $device ] = array( 'type' => 'string', 'default' => '' );
256 $extra[ 'bkbgBgHoverGradLoc2' . $device ] = array( 'type' => 'string', 'default' => '' );
257 $extra[ 'bkbgBgHoverGradAngle' . $device ] = array( 'type' => 'string', 'default' => '' );
258 $extra[ 'bkbgBgHoverGradPosition' . $device ] = array( 'type' => 'string', 'default' => '' );
259 }
260
261
262
263 // ── Border attributes ──
264
265 // Border Type (normal & hover)
266 $extra['bkbgBorderType'] = array( 'type' => 'string', 'default' => '' );
267 $extra['bkbgBorderHoverType'] = array( 'type' => 'string', 'default' => '' );
268
269 // Border Width — per side, per device, with unit (normal & hover)
270 foreach ( array( 'bkbgBorderWidth', 'bkbgBorderHoverWidth' ) as $prefix ) {
271 foreach ( $sides as $side ) {
272 foreach ( $devices as $device ) {
273 $key = $prefix . $side . $device;
274 $extra[ $key ] = array( 'type' => 'string', 'default' => '' );
275 $extra[ $key . 'Unit' ] = array( 'type' => 'string', 'default' => 'px' );
276 }
277 }
278 foreach ( $devices as $device ) {
279 $extra[ $prefix . 'Linked' . $device ] = array( 'type' => 'boolean', 'default' => true );
280 }
281 }
282
283 // Border Color (normal & hover)
284 $extra['bkbgBorderColor'] = array( 'type' => 'string', 'default' => '' );
285 $extra['bkbgBorderHoverColor'] = array( 'type' => 'string', 'default' => '' );
286
287 // Border Radius — per corner, per device, with unit (normal & hover)
288 foreach ( array( 'bkbgBorderRadius', 'bkbgBorderHoverRadius' ) as $prefix ) {
289 foreach ( $sides as $side ) {
290 foreach ( $devices as $device ) {
291 $key = $prefix . $side . $device;
292 $extra[ $key ] = array( 'type' => 'string', 'default' => '' );
293 $extra[ $key . 'Unit' ] = array( 'type' => 'string', 'default' => 'px' );
294 }
295 }
296 foreach ( $devices as $device ) {
297 $extra[ $prefix . 'Linked' . $device ] = array( 'type' => 'boolean', 'default' => true );
298 }
299 }
300
301 // Box Shadow (normal)
302 $extra['bkbgShadowColor'] = array( 'type' => 'string', 'default' => '' );
303 $extra['bkbgShadowH'] = array( 'type' => 'string', 'default' => '' );
304 $extra['bkbgShadowV'] = array( 'type' => 'string', 'default' => '' );
305 $extra['bkbgShadowBlur'] = array( 'type' => 'string', 'default' => '' );
306 $extra['bkbgShadowSpread'] = array( 'type' => 'string', 'default' => '' );
307 $extra['bkbgShadowPosition'] = array( 'type' => 'string', 'default' => '' );
308
309 // Box Shadow (hover)
310 $extra['bkbgShadowHoverColor'] = array( 'type' => 'string', 'default' => '' );
311 $extra['bkbgShadowHoverH'] = array( 'type' => 'string', 'default' => '' );
312 $extra['bkbgShadowHoverV'] = array( 'type' => 'string', 'default' => '' );
313 $extra['bkbgShadowHoverBlur'] = array( 'type' => 'string', 'default' => '' );
314 $extra['bkbgShadowHoverSpread'] = array( 'type' => 'string', 'default' => '' );
315 $extra['bkbgShadowHoverPosition'] = array( 'type' => 'string', 'default' => '' );
316
317
318 // Responsive visibility
319 $extra['bkbgHideDesktop'] = array( 'type' => 'boolean', 'default' => false );
320 $extra['bkbgHideTablet'] = array( 'type' => 'boolean', 'default' => false );
321 $extra['bkbgHideMobile'] = array( 'type' => 'boolean', 'default' => false );
322
323 if ( ! isset( $args['attributes'] ) || ! is_array( $args['attributes'] ) ) {
324 $args['attributes'] = array();
325 }
326 $args['attributes'] = array_merge( $args['attributes'], $extra );
327
328 return $args;
329 }, 10, 2 );
330
331 /**
332 * Render ALL advanced layout CSS (desktop + tablet + mobile) for Blockenberg blocks.
333 * Desktop styles are output as inline styles on the wrapper.
334 * Responsive styles use <style> tags with media queries.
335 * CSS ID / CSS Classes are also injected here for reliability.
336 */
337 add_filter( 'render_block', function ( $block_content, $block ) {
338 if ( empty( $block['blockName'] ) || strpos( $block['blockName'], 'blockenberg/' ) !== 0 ) {
339 return $block_content;
340 }
341
342 $attrs = $block['attrs'] ?? array();
343 if ( empty( $attrs ) ) {
344 return $block_content;
345 }
346
347 $sides = array( 'Top', 'Right', 'Bottom', 'Left' );
348 $devices = array( 'desktop', 'tablet', 'mobile' );
349
350 // Collect CSS rules per device
351 $css = array( 'desktop' => array(), 'tablet' => array(), 'mobile' => array() );
352
353 foreach ( array( 'bkbgMargin' => 'margin', 'bkbgPadding' => 'padding' ) as $prefix => $prop ) {
354 foreach ( $sides as $side ) {
355 foreach ( $devices as $dev ) {
356 $suffix = 'desktop' === $dev ? '' : ( 'tablet' === $dev ? 'Tablet' : 'Mobile' );
357 $val = isset( $attrs[ $prefix . $side . $suffix ] ) ? $attrs[ $prefix . $side . $suffix ] : '';
358 $unit = isset( $attrs[ $prefix . $side . $suffix . 'Unit' ] ) ? $attrs[ $prefix . $side . $suffix . 'Unit' ] : 'px';
359 if ( '' !== $val && '' !== trim( (string) $val ) ) {
360 $css[ $dev ][] = $prop . '-' . strtolower( $side ) . ':' . $val . $unit . ' !important';
361 }
362 }
363 }
364 }
365
366 // Z-Index per device
367 foreach ( $devices as $dev ) {
368 $suffix = 'desktop' === $dev ? '' : ( 'tablet' === $dev ? 'Tablet' : 'Mobile' );
369 $zi = isset( $attrs[ 'bkbgZIndex' . $suffix ] ) ? $attrs[ 'bkbgZIndex' . $suffix ] : '';
370 if ( '' !== $zi && '' !== trim( (string) $zi ) ) {
371 $css[ $dev ][] = 'z-index:' . intval( $zi );
372 $css[ $dev ][] = 'position:relative';
373 }
374 }
375
376 // ── Background CSS ──
377 $hover_css = array( 'desktop' => array(), 'tablet' => array(), 'mobile' => array() );
378
379 $bg_type = ! empty( $attrs['bkbgBgType'] ) ? $attrs['bkbgBgType'] : '';
380 $hover_type = ! empty( $attrs['bkbgBgHoverType'] ) ? $attrs['bkbgBgHoverType'] : '';
381
382 // Helper: build gradient value
383 $build_gradient = function ( $attrs, $prefix, $suffix ) {
384 $c1 = ! empty( $attrs[ $prefix . 'GradColor1' ] ) ? $attrs[ $prefix . 'GradColor1' ] : '';
385 $c2 = ! empty( $attrs[ $prefix . 'GradColor2' ] ) ? $attrs[ $prefix . 'GradColor2' ] : '';
386 if ( '' === $c1 && '' === $c2 ) return '';
387 if ( '' === $c1 ) $c1 = 'transparent';
388 if ( '' === $c2 ) $c2 = 'transparent';
389
390 // Location 1 — with fallback to desktop
391 $loc1 = '';
392 if ( '' !== $suffix && ! empty( $attrs[ $prefix . 'GradLoc1' . $suffix ] ) ) {
393 $loc1 = $attrs[ $prefix . 'GradLoc1' . $suffix ];
394 } elseif ( ! empty( $attrs[ $prefix . 'GradLoc1' ] ) ) {
395 $loc1 = $attrs[ $prefix . 'GradLoc1' ];
396 }
397
398 // Location 2
399 $loc2 = '';
400 if ( '' !== $suffix && ! empty( $attrs[ $prefix . 'GradLoc2' . $suffix ] ) ) {
401 $loc2 = $attrs[ $prefix . 'GradLoc2' . $suffix ];
402 } elseif ( ! empty( $attrs[ $prefix . 'GradLoc2' ] ) ) {
403 $loc2 = $attrs[ $prefix . 'GradLoc2' ];
404 }
405
406 // Angle
407 $angle = '';
408 if ( '' !== $suffix && isset( $attrs[ $prefix . 'GradAngle' . $suffix ] ) && '' !== $attrs[ $prefix . 'GradAngle' . $suffix ] ) {
409 $angle = $attrs[ $prefix . 'GradAngle' . $suffix ];
410 } elseif ( isset( $attrs[ $prefix . 'GradAngle' ] ) && '' !== $attrs[ $prefix . 'GradAngle' ] ) {
411 $angle = $attrs[ $prefix . 'GradAngle' ];
412 }
413
414 $type = ! empty( $attrs[ $prefix . 'GradType' ] ) ? $attrs[ $prefix . 'GradType' ] : 'linear';
415
416 $stop1 = esc_attr( $c1 ) . ( '' !== $loc1 ? ' ' . intval( $loc1 ) . '%' : '' );
417 $stop2 = esc_attr( $c2 ) . ( '' !== $loc2 ? ' ' . intval( $loc2 ) . '%' : '' );
418
419 if ( 'radial' === $type ) {
420 // Radial position
421 $pos = '';
422 if ( '' !== $suffix && ! empty( $attrs[ $prefix . 'GradPosition' . $suffix ] ) ) {
423 $pos = $attrs[ $prefix . 'GradPosition' . $suffix ];
424 } elseif ( ! empty( $attrs[ $prefix . 'GradPosition' ] ) ) {
425 $pos = $attrs[ $prefix . 'GradPosition' ];
426 }
427 $at_part = '' !== $pos ? ' at ' . esc_attr( $pos ) : '';
428 return 'radial-gradient(circle' . $at_part . ',' . $stop1 . ',' . $stop2 . ')';
429 }
430
431 $angle_part = '' !== $angle ? intval( $angle ) . 'deg,' : '';
432 return 'linear-gradient(' . $angle_part . $stop1 . ',' . $stop2 . ')';
433 };
434
435 // Helper: resolve responsive classic image settings (with desktop fallback)
436 $get_img_setting = function ( $attrs, $prefix, $prop, $suffix ) {
437 $val = ! empty( $attrs[ $prefix . $prop . $suffix ] ) ? $attrs[ $prefix . $prop . $suffix ] : '';
438 if ( '' === $val && '' !== $suffix ) {
439 $val = ! empty( $attrs[ $prefix . $prop ] ) ? $attrs[ $prefix . $prop ] : '';
440 }
441 return $val;
442 };
443
444 $render_classic_image_css = function ( $attrs, $prefix, &$target_css, $devices ) use ( $get_img_setting ) {
445 // Background color
446 if ( ! empty( $attrs[ $prefix . 'Color' ] ) ) {
447 $target_css['desktop'][] = 'background-color:' . esc_attr( $attrs[ $prefix . 'Color' ] ) . ' !important';
448 }
449 // Attachment (global, not responsive)
450 $attach = ! empty( $attrs[ $prefix . 'Attachment' ] ) ? esc_attr( $attrs[ $prefix . 'Attachment' ] ) : '';
451 if ( '' !== $attach ) {
452 $target_css['desktop'][] = 'background-attachment:' . $attach . ' !important';
453 }
454
455 foreach ( $devices as $dev ) {
456 $suffix = 'desktop' === $dev ? '' : ( 'tablet' === $dev ? 'Tablet' : 'Mobile' );
457 $img = ! empty( $attrs[ $prefix . 'Image' . $suffix ] ) ? $attrs[ $prefix . 'Image' . $suffix ] : '';
458 if ( '' === $img && '' !== $suffix ) {
459 $img = ! empty( $attrs[ $prefix . 'Image' ] ) ? $attrs[ $prefix . 'Image' ] : '';
460 }
461 if ( '' !== $img ) {
462 $target_css[ $dev ][] = 'background-image:url(' . esc_url( $img ) . ') !important';
463
464 // Position
465 $pos = $get_img_setting( $attrs, $prefix, 'Position', $suffix );
466 if ( 'custom' === $pos ) {
467 $cx = $get_img_setting( $attrs, $prefix, 'PositionCustomX', $suffix );
468 $cy = $get_img_setting( $attrs, $prefix, 'PositionCustomY', $suffix );
469 $cx = '' !== $cx ? intval( $cx ) . '%' : '50%';
470 $cy = '' !== $cy ? intval( $cy ) . '%' : '50%';
471 $target_css[ $dev ][] = 'background-position:' . $cx . ' ' . $cy . ' !important';
472 } elseif ( '' !== $pos ) {
473 $target_css[ $dev ][] = 'background-position:' . esc_attr( $pos ) . ' !important';
474 } else {
475 $target_css[ $dev ][] = 'background-position:center !important';
476 }
477
478 // Repeat
479 $repeat = $get_img_setting( $attrs, $prefix, 'Repeat', $suffix );
480 if ( '' !== $repeat ) {
481 $target_css[ $dev ][] = 'background-repeat:' . esc_attr( $repeat ) . ' !important';
482 }
483
484 // Size
485 $size = $get_img_setting( $attrs, $prefix, 'Size', $suffix );
486 if ( 'custom' === $size ) {
487 $sw = $get_img_setting( $attrs, $prefix, 'SizeCustomW', $suffix );
488 $sh = $get_img_setting( $attrs, $prefix, 'SizeCustomH', $suffix );
489 $sw = '' !== $sw ? intval( $sw ) . 'px' : 'auto';
490 $sh = '' !== $sh ? intval( $sh ) . 'px' : 'auto';
491 $target_css[ $dev ][] = 'background-size:' . $sw . ' ' . $sh . ' !important';
492 } elseif ( '' !== $size ) {
493 $target_css[ $dev ][] = 'background-size:' . esc_attr( $size ) . ' !important';
494 } else {
495 $target_css[ $dev ][] = 'background-size:cover !important';
496 }
497 }
498 }
499 };
500
501 // Normal — Classic
502 if ( 'classic' === $bg_type ) {
503 $render_classic_image_css( $attrs, 'bkbgBg', $css, $devices );
504 }
505
506 // Normal — Gradient
507 if ( 'gradient' === $bg_type ) {
508 foreach ( $devices as $dev ) {
509 $suffix = 'desktop' === $dev ? '' : ( 'tablet' === $dev ? 'Tablet' : 'Mobile' );
510 $grad = $build_gradient( $attrs, 'bkbgBg', $suffix );
511 if ( '' !== $grad ) {
512 $css[ $dev ][] = 'background-image:' . $grad . ' !important';
513 }
514 }
515 }
516
517 // Hover — Classic
518 if ( 'classic' === $hover_type ) {
519 $render_classic_image_css( $attrs, 'bkbgBgHover', $hover_css, $devices );
520 }
521
522 // Hover — Gradient
523 if ( 'gradient' === $hover_type ) {
524 foreach ( $devices as $dev ) {
525 $suffix = 'desktop' === $dev ? '' : ( 'tablet' === $dev ? 'Tablet' : 'Mobile' );
526 $grad = $build_gradient( $attrs, 'bkbgBgHover', $suffix );
527 if ( '' !== $grad ) {
528 $hover_css[ $dev ][] = 'background-image:' . $grad . ' !important';
529 }
530 }
531 }
532
533 // ── Border CSS ──
534
535 // Helper: build box-shadow value
536 $build_shadow = function ( $attrs, $prefix ) {
537 $h = isset( $attrs[ $prefix . 'H' ] ) && '' !== $attrs[ $prefix . 'H' ] ? intval( $attrs[ $prefix . 'H' ] ) : '';
538 $v = isset( $attrs[ $prefix . 'V' ] ) && '' !== $attrs[ $prefix . 'V' ] ? intval( $attrs[ $prefix . 'V' ] ) : '';
539 $blur = isset( $attrs[ $prefix . 'Blur' ] ) && '' !== $attrs[ $prefix . 'Blur' ] ? intval( $attrs[ $prefix . 'Blur' ] ) : '';
540 $spread = isset( $attrs[ $prefix . 'Spread' ] ) && '' !== $attrs[ $prefix . 'Spread' ] ? intval( $attrs[ $prefix . 'Spread' ] ) : '';
541 $color = ! empty( $attrs[ $prefix . 'Color' ] ) ? $attrs[ $prefix . 'Color' ] : '';
542 $pos = ! empty( $attrs[ $prefix . 'Position' ] ) ? $attrs[ $prefix . 'Position' ] : '';
543
544 if ( '' === $h && '' === $v && '' === $blur && '' === $spread && '' === $color ) return '';
545
546 $h = '' !== $h ? $h . 'px' : '0px';
547 $v = '' !== $v ? $v . 'px' : '0px';
548 $blur = '' !== $blur ? $blur . 'px' : '0px';
549 $spread = '' !== $spread ? $spread . 'px' : '0px';
550 $color = '' !== $color ? esc_attr( $color ) : 'rgba(0,0,0,0.5)';
551
552 $val = $h . ' ' . $v . ' ' . $blur . ' ' . $spread . ' ' . $color;
553 if ( 'inset' === $pos ) {
554 $val = 'inset ' . $val;
555 }
556 return $val;
557 };
558
559 // Helper: render border CSS for a prefix (normal or hover)
560 $render_border_css = function ( $attrs, $prefix_type, $prefix_width, $prefix_radius, $shadow_prefix, &$target_css, $devices ) use ( $build_shadow ) {
561 $border_type = ! empty( $attrs[ $prefix_type ] ) ? $attrs[ $prefix_type ] : '';
562
563 // Border type + width + color
564 if ( '' !== $border_type && 'none' !== $border_type ) {
565 $target_css['desktop'][] = 'border-style:' . esc_attr( $border_type ) . ' !important';
566
567 // Border color
568 $color_key = str_replace( 'Type', 'Color', $prefix_type );
569 if ( ! empty( $attrs[ $color_key ] ) ) {
570 $target_css['desktop'][] = 'border-color:' . esc_attr( $attrs[ $color_key ] ) . ' !important';
571 }
572
573 // Border width (responsive, per side)
574 foreach ( $devices as $dev ) {
575 $suffix = 'desktop' === $dev ? '' : ( 'tablet' === $dev ? 'Tablet' : 'Mobile' );
576 foreach ( array( 'Top', 'Right', 'Bottom', 'Left' ) as $side ) {
577 $val = isset( $attrs[ $prefix_width . $side . $suffix ] ) ? $attrs[ $prefix_width . $side . $suffix ] : '';
578 $unit = isset( $attrs[ $prefix_width . $side . $suffix . 'Unit' ] ) ? $attrs[ $prefix_width . $side . $suffix . 'Unit' ] : 'px';
579 if ( '' !== $val && '' !== trim( (string) $val ) ) {
580 $target_css[ $dev ][] = 'border-' . strtolower( $side ) . '-width:' . $val . $unit . ' !important';
581 }
582 }
583 }
584 } elseif ( 'none' === $border_type ) {
585 $target_css['desktop'][] = 'border:none !important';
586 }
587
588 // Border radius (responsive, per corner)
589 foreach ( $devices as $dev ) {
590 $suffix = 'desktop' === $dev ? '' : ( 'tablet' === $dev ? 'Tablet' : 'Mobile' );
591 $radius_parts = array();
592 foreach ( array( 'Top', 'Right', 'Bottom', 'Left' ) as $side ) {
593 $val = isset( $attrs[ $prefix_radius . $side . $suffix ] ) ? $attrs[ $prefix_radius . $side . $suffix ] : '';
594 $unit = isset( $attrs[ $prefix_radius . $side . $suffix . 'Unit' ] ) ? $attrs[ $prefix_radius . $side . $suffix . 'Unit' ] : 'px';
595 if ( '' !== $val && '' !== trim( (string) $val ) ) {
596 $radius_parts[ $side ] = $val . $unit;
597 }
598 }
599 if ( ! empty( $radius_parts ) ) {
600 // Map Top/Right/Bottom/Left to border-radius corners: TL TR BR BL
601 $tl = isset( $radius_parts['Top'] ) ? $radius_parts['Top'] : '0px';
602 $tr = isset( $radius_parts['Right'] ) ? $radius_parts['Right'] : '0px';
603 $br = isset( $radius_parts['Bottom'] ) ? $radius_parts['Bottom'] : '0px';
604 $bl = isset( $radius_parts['Left'] ) ? $radius_parts['Left'] : '0px';
605 $target_css[ $dev ][] = 'border-radius:' . $tl . ' ' . $tr . ' ' . $br . ' ' . $bl . ' !important';
606 }
607 }
608
609 // Box shadow
610 $shadow_val = $build_shadow( $attrs, $shadow_prefix );
611 if ( '' !== $shadow_val ) {
612 $target_css['desktop'][] = 'box-shadow:' . $shadow_val . ' !important';
613 }
614 };
615
616 // Normal border
617 $render_border_css( $attrs, 'bkbgBorderType', 'bkbgBorderWidth', 'bkbgBorderRadius', 'bkbgShadow', $css, $devices );
618
619 // Hover border
620 $render_border_css( $attrs, 'bkbgBorderHoverType', 'bkbgBorderHoverWidth', 'bkbgBorderHoverRadius', 'bkbgShadowHover', $hover_css, $devices );
621
622 // ── Responsive visibility ──
623 $hide_desktop = ! empty( $attrs['bkbgHideDesktop'] );
624 $hide_tablet = ! empty( $attrs['bkbgHideTablet'] );
625 $hide_mobile = ! empty( $attrs['bkbgHideMobile'] );
626
627 // Check if hover styles exist
628 $has_hover = ! empty( $hover_css['desktop'] ) || ! empty( $hover_css['tablet'] ) || ! empty( $hover_css['mobile'] );
629
630 // Any styles to output?
631 $has_responsive = $hide_desktop || $hide_tablet || $hide_mobile;
632 $has_styles = ! empty( $css['desktop'] ) || ! empty( $css['tablet'] ) || ! empty( $css['mobile'] ) || $has_hover || $has_responsive;
633 $has_id = ! empty( $attrs['bkbgCssId'] );
634 $has_cls = ! empty( $attrs['bkbgCssClasses'] );
635
636 if ( ! $has_styles && ! $has_id && ! $has_cls ) {
637 return $block_content;
638 }
639
640 // Generate a unique class for targeting this specific block instance
641 $unique = 'bkbg-adv-' . substr( md5( serialize( $attrs ) . wp_rand() ), 0, 8 );
642
643 // Inject unique class into the first HTML tag
644 $block_content = preg_replace(
645 '/(^\s*<[a-zA-Z][^>]*\bclass\s*=\s*")/',
646 '$1' . esc_attr( $unique ) . ' ',
647 $block_content,
648 1,
649 $count
650 );
651 if ( ! $count ) {
652 // No class attribute found — add one
653 $block_content = preg_replace(
654 '/(^\s*<[a-zA-Z][^\s>]*)/',
655 '$1 class="' . esc_attr( $unique ) . '"',
656 $block_content,
657 1
658 );
659 }
660
661 // Inject CSS ID
662 if ( $has_id ) {
663 $safe_id = esc_attr( $attrs['bkbgCssId'] );
664 $block_content = preg_replace(
665 '/(^\s*<[a-zA-Z][^>]*)/',
666 '$1 id="' . $safe_id . '"',
667 $block_content,
668 1
669 );
670 }
671
672 // Inject CSS Classes
673 if ( $has_cls ) {
674 $safe_cls = esc_attr( $attrs['bkbgCssClasses'] );
675 $block_content = preg_replace(
676 '/(^\s*<[a-zA-Z][^>]*\bclass\s*=\s*")/',
677 '$1' . $safe_cls . ' ',
678 $block_content,
679 1
680 );
681 }
682
683 // Build <style> tag
684 if ( $has_styles ) {
685 $sel = '.' . $unique;
686 $style = '';
687
688 if ( ! empty( $css['desktop'] ) ) {
689 $style .= $sel . '{' . implode( ';', $css['desktop'] ) . '}';
690 }
691 if ( ! empty( $css['tablet'] ) ) {
692 $style .= '@media(max-width:1024px){' . $sel . '{' . implode( ';', $css['tablet'] ) . '}}';
693 }
694 if ( ! empty( $css['mobile'] ) ) {
695 $style .= '@media(max-width:767px){' . $sel . '{' . implode( ';', $css['mobile'] ) . '}}';
696 }
697
698 // Hover rules
699 if ( ! empty( $hover_css['desktop'] ) ) {
700 $style .= $sel . ':hover{' . implode( ';', $hover_css['desktop'] ) . '}';
701 }
702 if ( ! empty( $hover_css['tablet'] ) ) {
703 $style .= '@media(max-width:1024px){' . $sel . ':hover{' . implode( ';', $hover_css['tablet'] ) . '}}';
704 }
705 if ( ! empty( $hover_css['mobile'] ) ) {
706 $style .= '@media(max-width:767px){' . $sel . ':hover{' . implode( ';', $hover_css['mobile'] ) . '}}';
707 }
708
709 // Responsive visibility: hide on specific devices
710 if ( $hide_desktop ) {
711 // Hide on desktop (>1024px)
712 $style .= '@media(min-width:1025px){' . $sel . '{display:none !important}}';
713 }
714 if ( $hide_tablet ) {
715 // Hide on tablet (768–1024px)
716 $style .= '@media(min-width:768px) and (max-width:1024px){' . $sel . '{display:none !important}}';
717 }
718 if ( $hide_mobile ) {
719 // Hide on mobile (≤767px)
720 $style .= '@media(max-width:767px){' . $sel . '{display:none !important}}';
721 }
722
723 $block_content .= '<style>' . $style . '</style>';
724 }
725
726 return $block_content;
727 }, 10, 2 );
728
729 /**
730 * Register REST API endpoint for Post Grid block.
731 *
732 * Route: /wp-json/blockenberg/v1/post-grid
733 * Method: GET
734 * Params:
735 * - type: string (posts|pages|any CPT)
736 * - orderby: string (date|title|comment_count|...)
737 * - order: string (asc|desc)
738 * - per_page: int
739 * - offset: int
740 * - page: int (1-based)
741 * - excerpt_len: int (optional)
742 */
743 add_action( 'rest_api_init', function () {
744 register_rest_route( 'blockenberg/v1', '/post-grid', array(
745 'methods' => 'GET',
746 'permission_callback' => '__return_true',
747 'args' => array(
748 'type' => array( 'sanitize_callback' => 'sanitize_key' ),
749 'orderby' => array( 'sanitize_callback' => 'sanitize_key' ),
750 'order' => array( 'sanitize_callback' => 'sanitize_text_field' ),
751 'per_page' => array( 'sanitize_callback' => 'absint' ),
752 'offset' => array( 'sanitize_callback' => 'absint' ),
753 'page' => array( 'sanitize_callback' => 'absint' ),
754 'excerpt_len' => array( 'sanitize_callback' => 'absint' ),
755 ),
756 'callback' => function ( WP_REST_Request $request ) {
757 $requested_type = sanitize_key( $request->get_param( 'type' ) ?: 'post' );
758 if ( 'posts' === $requested_type ) {
759 $requested_type = 'post';
760 } elseif ( 'pages' === $requested_type ) {
761 $requested_type = 'page';
762 }
763
764 $public_post_types = get_post_types( array( 'public' => true ), 'names' );
765 $post_type = in_array( $requested_type, $public_post_types, true ) ? $requested_type : 'post';
766
767 $requested_orderby = sanitize_key( $request->get_param( 'orderby' ) ?: 'date' );
768 $allowed_orderby = array( 'date', 'title', 'modified', 'comment_count', 'rand', 'menu_order' );
769 $orderby = in_array( $requested_orderby, $allowed_orderby, true ) ? $requested_orderby : 'date';
770 $order = strtolower( (string) $request->get_param( 'order' ) ) === 'asc' ? 'ASC' : 'DESC';
771 $per_page = max( 1, absint( $request->get_param( 'per_page' ) ?: 6 ) );
772 $per_page = min( 50, $per_page );
773 $offset = max( 0, absint( $request->get_param( 'offset' ) ?: 0 ) );
774 $offset = min( 5000, $offset );
775 $page = max( 1, absint( $request->get_param( 'page' ) ?: 1 ) );
776 $page = min( 200, $page );
777 $excerpt_len = max( 5, absint( $request->get_param( 'excerpt_len' ) ?: 18 ) );
778 $excerpt_len = min( 80, $excerpt_len );
779
780 // Short cache for public non-product queries.
781 $is_product_query = ( 'product' === $post_type );
782 $cache_key = '';
783 if ( ! $is_product_query ) {
784 $cache_key = 'bkbg_post_grid_' . md5( wp_json_encode( array(
785 'type' => $post_type,
786 'orderby' => $orderby,
787 'order' => $order,
788 'per_page' => $per_page,
789 'offset' => $offset,
790 'page' => $page,
791 'excerpt_len' => $excerpt_len,
792 ) ) );
793 $cached = get_transient( $cache_key );
794 if ( is_array( $cached ) ) {
795 return rest_ensure_response( $cached );
796 }
797 }
798
799 $q = new WP_Query( array(
800 'post_type' => $post_type,
801 'post_status' => 'publish',
802 'orderby' => $orderby,
803 'order' => $order,
804 'posts_per_page' => $per_page,
805 'offset' => $offset + ( ( $page - 1 ) * $per_page ),
806 'ignore_sticky_posts' => true,
807 'no_found_rows' => true,
808 ) );
809
810 $posts = array();
811 foreach ( $q->posts as $p ) {
812 $post_id = $p->ID;
813 $title = wp_strip_all_tags( get_the_title( $post_id ) );
814 $link = esc_url_raw( get_permalink( $post_id ) );
815 $image = esc_url_raw( (string) get_the_post_thumbnail_url( $post_id, 'medium_large' ) );
816 $date = wp_strip_all_tags( (string) get_the_date( '', $post_id ) );
817 $author = sanitize_text_field( (string) get_the_author_meta( 'display_name', $p->post_author ) );
818 $meta = trim( $date . ' · ' . $author );
819 $raw = get_post_field( 'post_excerpt', $post_id );
820 if ( '' === $raw ) {
821 $raw = get_post_field( 'post_content', $post_id );
822 }
823 $excerpt = wp_strip_all_tags( wp_trim_words( wp_strip_all_tags( $raw ), $excerpt_len, '' ) );
824
825 $item = array(
826 'id' => $post_id,
827 'title' => $title,
828 'link' => $link,
829 'image' => $image,
830 'meta' => $meta,
831 'excerpt' => $excerpt,
832 );
833
834 if ( 'product' === $post_type && function_exists( 'wc_get_product' ) ) {
835 $product = wc_get_product( $post_id );
836 if ( $product ) {
837 $item['price_html'] = wp_kses_post( $product->get_price_html() );
838 $item['add_to_cart'] = esc_url_raw( $product->add_to_cart_url() );
839 }
840 }
841
842 $posts[] = $item;
843 }
844
845 $payload = array( 'posts' => $posts );
846
847 if ( ! $is_product_query && '' !== $cache_key ) {
848 set_transient( $cache_key, $payload, 60 );
849 }
850
851 return rest_ensure_response( $payload );
852 },
853 ) );
854 } );
855
856 /**
857 * Simple local newsletter subscribe endpoint.
858 * Stores emails in an option array; extend as needed (e.g., to a custom table).
859 */
860 add_action( 'rest_api_init', function () {
861 register_rest_route( 'blockenberg/v1', '/subscribe', array(
862 'methods' => 'POST',
863 'permission_callback' => '__return_true',
864 'args' => array(
865 'email' => array( 'sanitize_callback' => 'sanitize_email' ),
866 'website' => array( 'sanitize_callback' => 'sanitize_text_field' ),
867 ),
868 'callback' => function ( WP_REST_Request $request ) {
869 $payload = $request->get_json_params();
870 if ( ! is_array( $payload ) ) {
871 $payload = array();
872 }
873
874 // Basic rate limiting by IP to reduce spam / abuse.
875 $ip = '';
876 if ( isset( $_SERVER['REMOTE_ADDR'] ) ) {
877 $ip = sanitize_text_field( wp_unslash( (string) $_SERVER['REMOTE_ADDR'] ) );
878 }
879 if ( '' !== $ip ) {
880 $key = 'bkbg_subscribe_' . md5( $ip );
881 $rl = get_transient( $key );
882 if ( is_array( $rl ) && isset( $rl['count'], $rl['start'] ) ) {
883 if ( $rl['count'] >= 5 ) {
884 return new WP_Error( 'rate_limited', __( 'Too many requests. Please try again later.', 'blockenberg' ), array( 'status' => 429 ) );
885 }
886 $rl['count']++;
887 set_transient( $key, $rl, 60 );
888 } else {
889 set_transient( $key, array( 'count' => 1, 'start' => time() ), 60 );
890 }
891 }
892
893 // Optional honeypot field (bots tend to fill it).
894 $honeypot = isset( $payload['website'] ) ? sanitize_text_field( (string) $payload['website'] ) : '';
895 if ( '' !== $honeypot ) {
896 return rest_ensure_response( array( 'ok' => true ) );
897 }
898
899 $email = isset( $payload['email'] ) ? sanitize_email( (string) $payload['email'] ) : '';
900 if ( empty( $email ) || ! is_email( $email ) ) {
901 return new WP_Error( 'invalid_email', __( 'Invalid email address', 'blockenberg' ), array( 'status' => 400 ) );
902 }
903
904 $list = get_option( 'blockenberg_newsletter_subscribers', array() );
905 if ( ! is_array( $list ) ) {
906 $list = array();
907 }
908
909 // Prevent unbounded option growth.
910 if ( count( $list ) >= 5000 ) {
911 return new WP_Error( 'storage_full', __( 'Subscriber list is full.', 'blockenberg' ), array( 'status' => 503 ) );
912 }
913
914 if ( ! in_array( $email, $list, true ) ) {
915 $list[] = $email;
916 update_option( 'blockenberg_newsletter_subscribers', $list, false );
917 }
918 return rest_ensure_response( array( 'ok' => true ) );
919 },
920 ) );
921 } );