esc_attr($show_background), esc_attr($background_color), esc_attr($font_family), esc_attr($location_ids), esc_attr($category_ids), $nav_style === '' ? '' : ' style="' . esc_attr($nav_style) . '"', esc_attr($this->contentSpacing($attributes, 'contentMargin', true)), esc_attr($this->headingAlign($attributes)), esc_attr($this->contentSpacing($attributes, 'headingMargin', true)), esc_attr($panel_content), esc_attr($service_columns), esc_attr($show_dashboard_button), esc_attr($dashboard_button_text), esc_attr($dashboard_button_url), esc_attr(PanelStyle::sanitize($attributes['styleVariant'] ?? '')) ); } /** * Where a step's heading sits, validated against the whitelist. * * Empty is the default and the answer for anything unrecognised: the * value reaches the stylesheet as a class name, and block attributes are * whatever was saved in the post. * * @param array $attributes Block attributes. * @return string */ protected function headingAlign(array $attributes): string { $value = $this->str($attributes['headingAlign'] ?? ''); return in_array($value, BookingButtonMarkup::HEADING_ALIGNS, true) ? $value : ''; } /** * The content column's padding or margin, as the `top,right,bottom,left` * string the mount node carries. * * Delegated to the markup builder so the inline panel and the popup * normalise a length the same way — the popup's copy travels through * BookingButtonMarkup already. * * @param array $attributes Block attributes. * @param string $key Attribute name. * @param bool $allow_negative Whether a negative length is meaningful. * @return string */ protected function contentSpacing(array $attributes, string $key, bool $allow_negative): string { return BookingButtonMarkup::contentSpacing($attributes[$key] ?? [], $allow_negative); } /** * Renders the popup trigger. The modal and the panel mount node are created * client-side on first click, so nothing panel-shaped is emitted here — only * the settings that panel will need, carried on the button. * * @param array $attributes Block attributes. * @return string */ protected function renderPopup(array $attributes): string { // Not declared in block.json: only the blocks actually in popup mode // need the trigger bundle. The panel bundle rides along as its // dependency, which is what makes the modal able to mount a panel. if (BookingButtonAssets::register()) { wp_enqueue_script(BookingButtonAssets::VIEW_HANDLE); wp_enqueue_style(BookingButtonAssets::VIEW_HANDLE); } // A full-width button fills this wrapper, so the wrapper has to fill the // container — which it does not do on its own once it is a flex or grid // item, as it is inside a Group block laid out in a row. Blocks saved // before width and alignment were separate controls carry the width in // the alignment value, the same normalisation render() makes. $is_full = ($attributes['buttonWidth'] ?? 'auto') === 'full' || ($attributes['buttonAlign'] ?? '') === 'full'; $wrapper_attributes = get_block_wrapper_attributes([ 'class' => 'rox-booking-button-wrap' . ($is_full ? ' rox-booking-button-wrap--full' : ''), ]); return BookingButtonMarkup::render( [ // The design the panel behind this button renders. 'styleVariant' => PanelStyle::sanitize($attributes['styleVariant'] ?? ''), 'text' => $attributes['buttonText'] ?? '', 'align' => $attributes['buttonAlign'] ?? 'left', 'alignTablet' => $attributes['buttonAlignTablet'] ?? '', 'alignMobile' => $attributes['buttonAlignMobile'] ?? '', 'width' => $attributes['buttonWidth'] ?? 'auto', 'size' => $attributes['buttonSize'] ?? 'medium', 'style' => $attributes['buttonStyle'] ?? 'filled', 'icon' => $attributes['buttonIcon'] ?? 'calendar', 'backgroundColor' => $attributes['buttonBackgroundColor'] ?? '', 'textColor' => $attributes['buttonTextColor'] ?? '', 'borderColor' => $attributes['buttonBorderColor'] ?? '', 'backgroundColorHover' => $attributes['buttonBackgroundColorHover'] ?? '', 'textColorHover' => $attributes['buttonTextColorHover'] ?? '', 'borderColorHover' => $attributes['buttonBorderColorHover'] ?? '', 'fillColor' => $attributes['buttonFillColor'] ?? '', 'fillColorHover' => $attributes['buttonFillColorHover'] ?? '', 'borderWidth' => $attributes['buttonBorderWidth'] ?? 1, 'borderWidthTablet' => $attributes['buttonBorderWidthTablet'] ?? null, 'borderWidthMobile' => $attributes['buttonBorderWidthMobile'] ?? null, 'borderWidthHover' => $attributes['buttonBorderWidthHover'] ?? null, 'borderStyle' => $attributes['buttonBorderStyle'] ?? 'solid', 'borderRadius' => $attributes['buttonBorderRadius'] ?? 6, 'borderRadiusTablet' => $attributes['buttonBorderRadiusTablet'] ?? null, 'borderRadiusMobile' => $attributes['buttonBorderRadiusMobile'] ?? null, 'padding' => $attributes['buttonPadding'] ?? [], 'paddingTablet' => $attributes['buttonPaddingTablet'] ?? [], 'paddingMobile' => $attributes['buttonPaddingMobile'] ?? [], 'margin' => $attributes['buttonMargin'] ?? [], 'marginTablet' => $attributes['buttonMarginTablet'] ?? [], 'marginMobile' => $attributes['buttonMarginMobile'] ?? [], 'buttonWidthSize' => $attributes['buttonWidthSize'] ?? null, 'buttonWidthSizeTablet' => $attributes['buttonWidthSizeTablet'] ?? null, 'buttonWidthSizeMobile' => $attributes['buttonWidthSizeMobile'] ?? null, 'iconSize' => $attributes['buttonIconSize'] ?? null, 'iconSizeTablet' => $attributes['buttonIconSizeTablet'] ?? null, 'iconSizeMobile' => $attributes['buttonIconSizeMobile'] ?? null, 'iconGap' => $attributes['buttonIconGap'] ?? null, 'iconGapTablet' => $attributes['buttonIconGapTablet'] ?? null, 'iconGapMobile' => $attributes['buttonIconGapMobile'] ?? null, 'iconOffsetY' => $attributes['buttonIconOffsetY'] ?? null, 'iconOffsetYTablet' => $attributes['buttonIconOffsetYTablet'] ?? null, 'iconOffsetYMobile' => $attributes['buttonIconOffsetYMobile'] ?? null, 'boxShadow' => $attributes['buttonBoxShadow'] ?? [], 'boxShadowHover' => $attributes['buttonBoxShadowHover'] ?? [], 'modalWidth' => $attributes['modalWidth'] ?? 1100, 'hideNavigation' => !empty($attributes['hideNavigation']), 'hideInfo' => !empty($attributes['hideInfo']), 'serviceColumns' => $attributes['serviceColumns'] ?? 2, 'showDashboardButton' => !array_key_exists('showDashboardButton', $attributes) || !empty($attributes['showDashboardButton']), 'dashboardButtonText' => $attributes['dashboardButtonText'] ?? '', 'dashboardButtonUrl' => $attributes['dashboardButtonUrl'] ?? '', 'contentMargin' => $attributes['contentMargin'] ?? [], 'headingAlign' => $this->headingAlign($attributes), 'headingMargin' => $attributes['headingMargin'] ?? [], 'panelContent' => $attributes['panelContent'] ?? [], // No agent lock from this block: a panel tied to one agent is // what the Single Agent Booking Panel block is for, so the // markup builder's "no lock" default stands. 'locationIds' => $attributes['locationIds'] ?? [], 'categoryIds' => $attributes['categoryIds'] ?? [], 'resetOnClose' => !empty($attributes['resetOnClose']), // The panel appearance controls are shared by both modes, so // they ride along to the mount node the modal builds. 'showPanelBackground' => $this->showBackground($attributes), 'panelBackgroundColor' => $this->str($attributes['backgroundColor'] ?? ''), 'fontFamily' => $this->fontStack($attributes), 'panelStyle' => $this->panelStyle($attributes), ], $wrapper_attributes ); } /** * The panel's own custom properties: the Back / Next styling plus the accent * every other panel colour is derived from. * * The accent rides on the same declaration string as the nav variables: the * stylesheet derives every lighter shade and translucent wash from this one * property, and the two nav buttons still win where they set their own * colour. Empty leaves the stylesheet's own blue in force. * * @param array $attributes Block attributes. * @return string Semicolon-separated declarations, or '' when nothing is set. */ protected function panelStyle(array $attributes): string { $style = NavButtons::cssVars($attributes); $accent_color = Color::sanitize($this->str($attributes['accentColor'] ?? '')); if ($accent_color !== '') { $style = rtrim('--rox-accent:' . $accent_color . ';' . $style, ';'); } return $style; } /** * Whether the panel draws its own grey frame. * * Defaults to on, so only an explicit false switches it off. * * @param array $attributes Block attributes. * @return bool */ protected function showBackground(array $attributes): bool { return PanelStyle::showsBackground( $attributes['showBackground'] ?? null, (string) ($attributes['styleVariant'] ?? '') ); } /** * Resolves the chosen font to a CSS stack and queues its web font. * * @param array $attributes Block attributes. * @return string CSS font stack, or '' to keep the stylesheet's own font. */ protected function fontStack(array $attributes): string { $font_key = $this->str($attributes['fontFamily'] ?? ''); FontFamily::enqueue($font_key); return FontFamily::stack($font_key); } /** * Reads an attribute as a string. * * Casting straight to string is not safe: block.json's schema does not * validate an `object` attribute's inner values, so a hand-edited or * migrated post could hand an array to `(string)` — a warning at best. * * @param mixed $value Raw attribute. * @return string Trimmed string, or '' when the value is not stringable. */ protected function str($value): string { return is_scalar($value) ? trim((string) $value) : ''; } }