slugPrefix = App::config()->get('app.slug');
}
private function addReactSupport()
{
if (!static::$isReactSupportAdded) {
if (Vite::underDevelopment()) {
Vite::enqueueScript(
'react-support',
'admin/BlockEditor/ReactSupport.js',
['wp-blocks', 'wp-components']
);
}
static::$isReactSupportAdded = true;
}
}
abstract public function render(array $shortCodeAttribute, $block = null);
protected function generateEnqueueSlug(): string
{
return Str::of(
$this->slugPrefix . '_' . static::getEditorName()
)->snake('')->replace('-', '_')->toString();
}
public static function getEditorName(): string
{
return static::$editorName;
}
public static function register()
{
add_action('init', [static::make(), 'init']);
}
private function enqueueAsset()
{
$this->addReactSupport();
$this->enqueueScripts();
$this->enqueueStyles();
$this->enqueueGlobalStyles();
}
private function enqueueGlobalStyles()
{
Vite::enqueueStyle(
'fluent-cart-global-block-editor',
'admin/BlockEditor/Components/style/fct-global-block-editor.scss',
);
}
public function init(): void
{
add_action('enqueue_block_editor_assets', function () {
$this->enqueueAsset(); // Enqueue block editor-specific JS/CSS here
});
add_action('enqueue_block_assets', function () {
if($this->isBlockEditor()) {
// Reset flag so styles also load inside the WP 6.3+ editor iframe
// (enqueue_block_editor_assets already set it on the outer admin page)
$this->isStylesLoaded = false;
$this->enqueueStyles();
}
});
$blockArgs = [
'api_version' => 3,
'version' => 3,
'category' => 'fluent-cart',
'editor_script' => $this->getScriptName(),
'editor_css' => $this->getStyleName(),
'render_callback' => [$this, 'render_block'],
'provides_context' => $this->provideContext(),
'uses_context' => $this->useContext(),
'supports' => $this->supports()
];
$ancestor = $this->ancestor();
if (!empty($ancestor)) {
$blockArgs['ancestor'] = $ancestor;
}
$attributes = $this->blockAttributes();
if (!empty($attributes)) {
$blockArgs['attributes'] = $attributes;
}
if ($this->skipInnerBlocks()) {
$blockArgs['skip_inner_blocks'] = true;
}
register_block_type($this->slugPrefix . '/' . static::getEditorName(), $blockArgs);
}
public function blockAttributes(): array
{
return [];
}
/**
* Product for blocks that support a query_type attribute — same
* resolution ProductInfoBlockEditor::render() uses: 'custom' +
* product_id renders the picked product, anything else falls back
* to the current product context (single product page, Product
* Info wrapper, shop loops).
*
* @return object|null Product model, or null when nothing resolves
*/
/**
* The block's wrapper around what it rendered — or nothing at all when
* it rendered nothing. A review block on a product with reviews turned
* off would otherwise leave an empty div with the block's classes
* behind, which is a stray gap on the page and a false hook for styles.
*
* @param string $wrapperAttributes get_block_wrapper_attributes() output
* @param string $inner
* @return string
*/
protected static function wrapUnlessEmpty(string $wrapperAttributes, string $inner): string
{
if (trim($inner) === '') {
return '';
}
return '
' . $inner . '
';
}
protected function resolveProduct(array $attributes)
{
if (Arr::get($attributes, 'query_type', 'default') === 'custom') {
$productId = absint(Arr::get($attributes, 'product_id', 0));
if (!$productId) {
return null;
}
// Published only, so a hand-edited product_id cannot surface a
// draft/private product on a public page. No eager loading —
// callers only read the ID; a caller that needs relations loads
// them itself.
return \FluentCart\App\Models\Product::query()
->where('post_status', 'publish')
->find($productId);
}
return fluent_cart_get_current_product();
}
/**
* Register just the block type (render callback, attributes, supports)
* without editor scripts/styles. Used for email-only blocks that handle
* asset enqueuing through the email editor handler instead of globally.
*/
public function registerBlockType(): void
{
$blockArgs = [
'api_version' => 3,
'category' => 'fluent-cart',
'render_callback' => [$this, 'render_block'],
'supports' => $this->supports(),
];
$attributes = $this->blockAttributes();
if (!empty($attributes)) {
$blockArgs['attributes'] = $attributes;
}
register_block_type($this->slugPrefix . '/' . static::getEditorName(), $blockArgs);
}
public function supports(): array
{
return [
'renaming' => false,
'innerBlocks' => true,
'align' => true,
];
}
public function ancestor(): array
{
return [];
}
public function provideContext()
{
return null;
}
public function useContext()
{
return null;
}
/**
* Whether to skip automatic inner block rendering in WP_Block::render().
*
* Override to return true in blocks that manually render their inner blocks
* in the render callback. This prevents WordPress from auto-rendering inner
* blocks before the callback runs (which causes double rendering and can
* trigger WP 6.x's empty-block script dequeue mechanism).
*/
protected function skipInnerBlocks(): bool
{
return false;
}
public function render_block($attributes, $content, $block)
{
$prefix = '';
if (!self::$blockSupportStylesEnqueued && !is_admin()) {
$prefix = self::getBlockSupportFallbackStyles();
self::$blockSupportStylesEnqueued = true;
}
$attributes = Arr::wrap($attributes);
return $prefix . $this->render($attributes, $block, $content);
}
/**
* Ensure WordPress global styles (preset colors, typography, spacing) are available
* on the frontend. Some themes (e.g., Bricks) strip the global-styles stylesheet,
* which breaks block support classes like .has-vivid-red-color.
*
* This outputs a minimal inline fallback only if global styles aren't already loaded.
*/
private static function getBlockSupportFallbackStyles(): string
{
// Skip if global styles are already loaded by the theme
if (wp_style_is('global-styles', 'done') || wp_style_is('global-styles', 'enqueued')) {
return '';
}
// Use WordPress API to get preset variables and class rules
if (function_exists('wp_get_global_stylesheet')) {
$css = wp_get_global_stylesheet(['variables', 'presets']);
if (!empty($css)) {
return '';
}
}
return '';
}
public function getStyles(): array
{
return [];
}
public static function make(): BlockEditor
{
return new static();
}
protected function isBlockEditor(): bool
{
if (!function_exists('get_current_screen')) {
require_once ABSPATH . '/wp-admin/includes/screen.php';
}
$current_screen = \get_current_screen();
// Check for regular block editor (posts/pages)
if ($current_screen instanceof \WP_Screen && $current_screen->is_block_editor()) {
return true;
}
// Check for site editor (theme templates)
$path = wp_unslash(App::request()->get('path'));
if (is_admin() && isset($path) && strpos($path, '/wp_template') !== false) {
return true;
}
return false;
}
}