PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.0
1.7.0 1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 All 50 releases
fluent-cart / app / Hooks / Handlers / BlockEditors / BlockEditor.php

BlockEditor.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.7.0, at app/Hooks/Handlers/BlockEditors/BlockEditor.php

302 lines 9.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Hooks\Handlers\BlockEditors;
4
5 use FluentCart\Api\Contracts\CanEnqueue;
6 use FluentCart\App\App;
7 use FluentCart\App\Vite;
8 use FluentCart\Framework\Support\Arr;
9 use FluentCart\Framework\Support\Str;
10
11 abstract class BlockEditor
12 {
13 use CanEnqueue;
14
15 const PARENT_BLOCK_DATA_NAME = 'fluent_cart_parent_block_data';
16
17 protected static string $editorName;
18 private static bool $isReactSupportAdded = false;
19 private static bool $blockSupportStylesEnqueued = false;
20
21 public function __construct()
22 {
23 $this->slugPrefix = App::config()->get('app.slug');
24 }
25
26 private function addReactSupport()
27 {
28 if (!static::$isReactSupportAdded) {
29 if (Vite::underDevelopment()) {
30 Vite::enqueueScript(
31 'react-support',
32 'admin/BlockEditor/ReactSupport.js',
33 ['wp-blocks', 'wp-components']
34 );
35 }
36 static::$isReactSupportAdded = true;
37 }
38 }
39
40 abstract public function render(array $shortCodeAttribute, $block = null);
41
42 protected function generateEnqueueSlug(): string
43 {
44 return Str::of(
45 $this->slugPrefix . '_' . static::getEditorName()
46 )->snake('')->replace('-', '_')->toString();
47 }
48
49 public static function getEditorName(): string
50 {
51 return static::$editorName;
52 }
53
54 public static function register()
55 {
56 add_action('init', [static::make(), 'init']);
57 }
58
59 private function enqueueAsset()
60 {
61 $this->addReactSupport();
62 $this->enqueueScripts();
63 $this->enqueueStyles();
64 $this->enqueueGlobalStyles();
65 }
66
67 private function enqueueGlobalStyles()
68 {
69 Vite::enqueueStyle(
70 'fluent-cart-global-block-editor',
71 'admin/BlockEditor/Components/style/fct-global-block-editor.scss',
72 );
73 }
74
75 public function init(): void
76 {
77 add_action('enqueue_block_editor_assets', function () {
78 $this->enqueueAsset(); // Enqueue block editor-specific JS/CSS here
79 });
80 add_action('enqueue_block_assets', function () {
81 if($this->isBlockEditor()) {
82 // Reset flag so styles also load inside the WP 6.3+ editor iframe
83 // (enqueue_block_editor_assets already set it on the outer admin page)
84 $this->isStylesLoaded = false;
85 $this->enqueueStyles();
86 }
87 });
88
89 $blockArgs = [
90 'api_version' => 3,
91 'version' => 3,
92 'category' => 'fluent-cart',
93 'editor_script' => $this->getScriptName(),
94 'editor_css' => $this->getStyleName(),
95 'render_callback' => [$this, 'render_block'],
96 'provides_context' => $this->provideContext(),
97 'uses_context' => $this->useContext(),
98 'supports' => $this->supports()
99 ];
100
101 $ancestor = $this->ancestor();
102 if (!empty($ancestor)) {
103 $blockArgs['ancestor'] = $ancestor;
104 }
105
106 $attributes = $this->blockAttributes();
107 if (!empty($attributes)) {
108 $blockArgs['attributes'] = $attributes;
109 }
110
111 if ($this->skipInnerBlocks()) {
112 $blockArgs['skip_inner_blocks'] = true;
113 }
114
115 register_block_type($this->slugPrefix . '/' . static::getEditorName(), $blockArgs);
116
117 }
118
119 public function blockAttributes(): array
120 {
121 return [];
122 }
123
124 /**
125 * Product for blocks that support a query_type attribute — same
126 * resolution ProductInfoBlockEditor::render() uses: 'custom' +
127 * product_id renders the picked product, anything else falls back
128 * to the current product context (single product page, Product
129 * Info wrapper, shop loops).
130 *
131 * @return object|null Product model, or null when nothing resolves
132 */
133 /**
134 * The block's wrapper around what it rendered — or nothing at all when
135 * it rendered nothing. A review block on a product with reviews turned
136 * off would otherwise leave an empty div with the block's classes
137 * behind, which is a stray gap on the page and a false hook for styles.
138 *
139 * @param string $wrapperAttributes get_block_wrapper_attributes() output
140 * @param string $inner
141 * @return string
142 */
143 protected static function wrapUnlessEmpty(string $wrapperAttributes, string $inner): string
144 {
145 if (trim($inner) === '') {
146 return '';
147 }
148
149 return '<div ' . $wrapperAttributes . '>' . $inner . '</div>';
150 }
151
152 protected function resolveProduct(array $attributes)
153 {
154 if (Arr::get($attributes, 'query_type', 'default') === 'custom') {
155 $productId = absint(Arr::get($attributes, 'product_id', 0));
156
157 if (!$productId) {
158 return null;
159 }
160
161 // Published only, so a hand-edited product_id cannot surface a
162 // draft/private product on a public page. No eager loading —
163 // callers only read the ID; a caller that needs relations loads
164 // them itself.
165 return \FluentCart\App\Models\Product::query()
166 ->where('post_status', 'publish')
167 ->find($productId);
168 }
169
170 return fluent_cart_get_current_product();
171 }
172
173 /**
174 * Register just the block type (render callback, attributes, supports)
175 * without editor scripts/styles. Used for email-only blocks that handle
176 * asset enqueuing through the email editor handler instead of globally.
177 */
178 public function registerBlockType(): void
179 {
180 $blockArgs = [
181 'api_version' => 3,
182 'category' => 'fluent-cart',
183 'render_callback' => [$this, 'render_block'],
184 'supports' => $this->supports(),
185 ];
186
187 $attributes = $this->blockAttributes();
188 if (!empty($attributes)) {
189 $blockArgs['attributes'] = $attributes;
190 }
191
192 register_block_type($this->slugPrefix . '/' . static::getEditorName(), $blockArgs);
193 }
194
195 public function supports(): array
196 {
197 return [
198 'renaming' => false,
199 'innerBlocks' => true,
200 'align' => true,
201 ];
202 }
203
204 public function ancestor(): array
205 {
206 return [];
207 }
208
209 public function provideContext()
210 {
211 return null;
212 }
213
214 public function useContext()
215 {
216 return null;
217 }
218
219 /**
220 * Whether to skip automatic inner block rendering in WP_Block::render().
221 *
222 * Override to return true in blocks that manually render their inner blocks
223 * in the render callback. This prevents WordPress from auto-rendering inner
224 * blocks before the callback runs (which causes double rendering and can
225 * trigger WP 6.x's empty-block script dequeue mechanism).
226 */
227 protected function skipInnerBlocks(): bool
228 {
229 return false;
230 }
231
232 public function render_block($attributes, $content, $block)
233 {
234 $prefix = '';
235 if (!self::$blockSupportStylesEnqueued && !is_admin()) {
236 $prefix = self::getBlockSupportFallbackStyles();
237 self::$blockSupportStylesEnqueued = true;
238 }
239
240 $attributes = Arr::wrap($attributes);
241 return $prefix . $this->render($attributes, $block, $content);
242 }
243
244 /**
245 * Ensure WordPress global styles (preset colors, typography, spacing) are available
246 * on the frontend. Some themes (e.g., Bricks) strip the global-styles stylesheet,
247 * which breaks block support classes like .has-vivid-red-color.
248 *
249 * This outputs a minimal inline fallback only if global styles aren't already loaded.
250 */
251 private static function getBlockSupportFallbackStyles(): string
252 {
253 // Skip if global styles are already loaded by the theme
254 if (wp_style_is('global-styles', 'done') || wp_style_is('global-styles', 'enqueued')) {
255 return '';
256 }
257
258 // Use WordPress API to get preset variables and class rules
259 if (function_exists('wp_get_global_stylesheet')) {
260 $css = wp_get_global_stylesheet(['variables', 'presets']);
261 if (!empty($css)) {
262 return '<style id="fluent-cart-block-supports-css">' . wp_strip_all_tags($css) . '</style>';
263 }
264 }
265
266 return '';
267 }
268
269
270 public function getStyles(): array
271 {
272 return [];
273 }
274
275
276 public static function make(): BlockEditor
277 {
278 return new static();
279 }
280
281 protected function isBlockEditor(): bool
282 {
283 if (!function_exists('get_current_screen')) {
284 require_once ABSPATH . '/wp-admin/includes/screen.php';
285 }
286 $current_screen = \get_current_screen();
287
288 // Check for regular block editor (posts/pages)
289 if ($current_screen instanceof \WP_Screen && $current_screen->is_block_editor()) {
290 return true;
291 }
292
293 // Check for site editor (theme templates)
294 $path = wp_unslash(App::request()->get('path'));
295 if (is_admin() && isset($path) && strpos($path, '/wp_template') !== false) {
296 return true;
297 }
298
299 return false;
300 }
301 }
302