does not
* help there: JavaScript is ON, it just never ran bfields.
*
* So the escape hatch is ordinary server HTML INSIDE the root. The bundle
* removes it when it mounts; if the bundle never runs, it stays visible
* (review 3.6). The same URL rides on `data-fallback` for the error boundary,
* which covers a bundle that loads and then fails to render.
*
* It first shows a skeleton of the screen, and turns into the notice only once the bundle has
* had its chance: when it gave up, or `bfields_fallback_delay` (5 s) after the
* page's scripts have run. A slow page therefore never looks broken while it
* is still loading (3D Viewer integration demo, 27 Sep 2026).
*
* @package BFields
*/
namespace BFields;
if (!defined('ABSPATH')) {
exit;
}
/**
* Prints `.bfields-root` with its fallback notice.
*/
final class Mount
{
/**
* Print the root for one screen.
*
* @param string $unique Storage key.
* @param string $kind 'options' | 'metabox'.
* @param array $schema Normalised schema.
* @param array $data Extra data-* attributes (name => value).
*/
public static function render(string $unique, string $kind, array $schema, array $data = array()): void
{
$style = '';
foreach ($schema['args']['brand'] as $token => $value) {
if ('logo' === $token) {
continue;
}
$style .= '--bfields-' . sanitize_key($token) . ':' . $value . ';';
}
$fallback = self::fallback_url($unique);
$attrs = '';
$data = array_merge(array('unique' => $unique, 'kind' => $kind, 'fallback' => $fallback), $data);
foreach ($data as $name => $value) {
if ('' === (string) $value) {
continue;
}
$attrs .= sprintf(' data-%s="%s"', sanitize_key($name), esc_attr((string) $value));
}
// `bfields-app` is the token + reset scope every bfields stylesheet is
// written under (theme/base.css rule 1); `bfields-root` is the mount
// hook the bundle looks for. The root element carries both.
printf(
'
',
$attrs, // Escaped above.
'' === $style ? '' : ' style="' . esc_attr($style) . '"'
);
// The escape hatch has two states. The bundle removes the whole
// `.bfields-fallback` when it mounts. Until then it shows a skeleton, because
// the bundle mounts only when the page's scripts have all run, which on a
// heavy admin page or a slow host can take a while, and a failure message
// there reads as a broken screen. Only when the bundle has had its chance
// does it say so and offer the host's switch. Both states are plain HTML
// plus an inline style and script: they have to work when the bundle, and
// bfields' own stylesheet, never arrive. With JavaScript off, the second
// state shows at once.
self::fallback_assets();
echo '
';
echo esc_html(
'metabox' === $kind
? __('These settings are taking longer than usual to load, or they could not load on this site. Nothing has changed: saving the post will not change them.', 'bfields')
: __('This screen is taking longer than usual to load, or it could not load on this site. Your saved settings are safe and unchanged.', 'bfields')
);
if ('' !== $fallback) {
echo ' ' . esc_html__('Switch to the classic interface', 'bfields') . '';
}
echo '
';
}
// The side column as EditorShell draws it: preview, Publish, Save Draft on a draft, the shortcode bar.
$side = $bar('bfields-skel__preview') . $bar('bfields-skel__button bfields-skel__button--primary')
. (empty($editor['published']) ? $bar('bfields-skel__button') : '')
. ('' !== $chip ? $bar('bfields-skel__button') : '');
$body = '
' . $cards . '
' . $side . '
';
// The real column goes full width by setting this on ; the rule leaves with the fallback.
$width = 'full' === ($schema['args']['pageWidth'] ?? '') ? '' : '';
return $width . '
';
}
/**
* What switches the fallback's two states, once per page, inline.
*
* The bundle is a footer script and mounts on DOMContentLoaded, so right
* after that event it has either mounted a root (the fallback is gone),
* given up on it (the root is marked but the fallback is still there), or
* not run at all. The script shows the notice at once in the second case,
* and after `bfields_fallback_delay` in the third, which leaves room for a
* bundle an optimisation plugin made load late.
*
* Without the script (JavaScript off, inline scripts blocked, a script
* earlier on the page that never finishes loading) CSS does it: