PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor / 2.0.6
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor v2.0.6
2.0.13 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 & AI Agent for WordPress Block Editor 2.0.6, 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.6
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 } );