get_theme_styles(); $flex_gap = $theme_styles['spacing']['blockGap'] ?? '0px'; $flex_gap_number = Styles_Helper::parse_value( $flex_gap ); $margin_top = $parsed_block['email_attrs']['margin-top'] ?? '0px'; $justify = $rendering_context->resolve_text_align( $parsed_block['attrs']['layout']['justifyContent'] ?? null ); $styles = wp_style_engine_get_styles( $parsed_block['attrs']['style'] ?? array() )['css'] ?? ''; $styles .= 'margin-top: ' . $margin_top . ';'; $styles .= 'text-align: ' . $justify; list( $inner_blocks, $should_wrap ) = $this->compute_widths_for_flex_layout( $parsed_block, $flex_gap_number ); if ( $should_wrap ) { return $this->render_wrapping_layout( $inner_blocks, $styles, $justify, $flex_gap, $rendering_context ); } return $this->render_single_row_layout( $inner_blocks, $styles, $justify, $flex_gap, $rendering_context ); } /** * Render the inner blocks as a single, non-wrapping row. * * This is the default layout: a table row whose cells sit side by side. It's correct whenever * the items are known to fit the available width (and it's what every explicitly-sized buttons * block uses, since those are shrunk to fit by {@see compute_widths_for_flex_layout()}). * * @param array $inner_blocks Inner blocks with computed widths. * @param string $styles Wrapper styles (already includes margin-top and text-align). * @param string $justify Resolved horizontal alignment (left/center/right). * @param string $flex_gap Gap between items (e.g. "16px"). * @param Rendering_Context $rendering_context Rendering context. * @return string */ private function render_single_row_layout( array $inner_blocks, string $styles, string $justify, string $flex_gap, Rendering_Context $rendering_context ): string { // MS Outlook doesn't support style attribute in divs so we conditionally wrap the buttons in a table and repeat styles. $output_html = sprintf( '
', esc_attr( $styles ), esc_attr( $justify ) ); foreach ( $inner_blocks as $key => $block ) { $item_styles = array(); if ( $block['email_attrs']['layout_width'] ?? null ) { $item_styles['width'] = $block['email_attrs']['layout_width']; } if ( $key > 0 ) { $item_styles[ 'padding-' . $rendering_context->get_start_side() ] = $flex_gap; } $output_html .= ''; } $output_html .= '
' . render_block( $block ) . '
'; return $output_html; } /** * Render the inner blocks so they wrap when they don't fit on one line. * * Used for auto-width button rows (e.g. a footer navigation menu) whose combined width exceeds * the available width. A table row can't reflow in email, so each item is emitted as an * inline-block
: clients that honor it (Gmail desktop, Apple Mail, most webmail) flow the * items and wrap them onto multiple lines instead of stretching the email past its content * width. MS Outlook (Word engine) ignores inline-block and can't wrap a row either, so an * Outlook-only
before each item after the first forces it to stack the buttons vertically * — no overflow, at the cost of a vertical list rather than a grid. Fixes NL-737. * * @param array $inner_blocks Inner blocks (at least one is auto-width in this path). * @param string $styles Wrapper styles (already includes margin-top and text-align). * @param string $justify Resolved horizontal alignment (left/center/right). * @param string $flex_gap Gap between items (e.g. "16px"). * @param Rendering_Context $rendering_context Rendering context. * @return string */ private function render_wrapping_layout( array $inner_blocks, string $styles, string $justify, string $flex_gap, Rendering_Context $rendering_context ): string { // Outlook ignores style on divs, so repeat the wrapper styles on a conditional table cell. $output_html = sprintf( '
', esc_attr( $styles ), esc_attr( $justify ) ); // The gap padding must sit on the side away from the aligned (flush) edge, otherwise wrapped // lines carry an edge pad and misalign against each other. When items align to the end side // of the line flow (e.g. right in LTR), pad the start side of every item after the first; // otherwise (start-aligned or centered) pad the end side of every item before the last. $align_to_flow_end = $justify === $rendering_context->get_end_side(); $last_key = count( $inner_blocks ) - 1; foreach ( $inner_blocks as $key => $block ) { $item_styles = array( 'display' => 'inline-block', 'vertical-align' => 'top', // Vertical gap so items that wrap onto a new line (and Outlook's stacked buttons) don't touch. 'padding-bottom' => $flex_gap, ); if ( $block['email_attrs']['layout_width'] ?? null ) { $item_styles['width'] = $block['email_attrs']['layout_width']; } if ( $align_to_flow_end && $key > 0 ) { $item_styles[ 'padding-' . $rendering_context->get_start_side() ] = $flex_gap; } elseif ( ! $align_to_flow_end && $key < $last_key ) { $item_styles[ 'padding-' . $rendering_context->get_end_side() ] = $flex_gap; } if ( $key > 0 ) { // Force a line break before every item after the first in Outlook, which can't wrap an inline row. $output_html .= ''; } $output_html .= '
' . render_block( $block ) . '
'; } $output_html .= '
'; return $output_html; } /** * Compute widths for blocks in flex layout and decide whether the row must wrap. * * @param array $parsed_block Parsed block. * @param float $flex_gap Flex gap. * @return array{0: array, 1: bool} The inner blocks (with computed widths) and whether the row * should wrap instead of rendering as a single row. */ private function compute_widths_for_flex_layout( array $parsed_block, float $flex_gap ): array { // When there is no parent width we can't compute widths so auto width will be used, and we // can't tell whether the items overflow — fall back to the (non-wrapping) single row. if ( ! isset( $parsed_block['email_attrs']['width'] ) ) { return array( $parsed_block['innerBlocks'] ?? array(), false ); } $blocks_count = count( $parsed_block['innerBlocks'] ); $total_used_width = 0; // Total width assuming items without set width would consume proportional width. $parent_width = Styles_Helper::parse_value( $parsed_block['email_attrs']['width'] ); $inner_blocks = $parsed_block['innerBlocks'] ?? array(); $has_auto_width = false; foreach ( $inner_blocks as $key => $block ) { $block_width_percent = ( $block['attrs']['width'] ?? 0 ) ? intval( $block['attrs']['width'] ) : 0; $block_width = floor( $parent_width * ( $block_width_percent / 100 ) ); // If width is not set, we assume it's 25% of the parent width. $total_used_width += $block_width ? $block_width : floor( $parent_width * ( 25 / 100 ) ); if ( ! $block_width ) { $has_auto_width = true; $inner_blocks[ $key ]['email_attrs']['layout_width'] = null; // Will be rendered as auto. continue; } $inner_blocks[ $key ]['email_attrs']['layout_width'] = $this->get_width_without_gap( $block_width, $flex_gap, $block_width_percent ) . 'px'; } $overflows = $blocks_count > 1 && $total_used_width > $parent_width; // When there is only one block, or the items fit, render the single row as set by the user. if ( ! $overflows ) { return array( $inner_blocks, false ); } // Auto-width items (e.g. a nav menu of buttons) can't be shrunk to fit, so an overflowing row // that contains any of them can only be kept inside the content width by wrapping — the // explicitly-sized items in the same row keep their width and flow alongside. A row made up // entirely of explicit-width items is instead scaled down proportionally so it still fits on // one row, preserving the existing behavior. if ( $has_auto_width ) { return array( $inner_blocks, true ); } foreach ( $inner_blocks as $key => $block ) { $proportional_space_overflow = $parent_width / $total_used_width; $block_width = $block['email_attrs']['layout_width'] ? Styles_Helper::parse_value( $block['email_attrs']['layout_width'] ) : 0; $block_proportional_width = $block_width * $proportional_space_overflow; $block_proportional_percentage = ( $block_proportional_width / $parent_width ) * 100; $inner_blocks[ $key ]['email_attrs']['layout_width'] = $block_width ? $this->get_width_without_gap( $block_proportional_width, $flex_gap, $block_proportional_percentage ) . 'px' : null; } return array( $inner_blocks, false ); } /** * How much of width we will strip to keep some space for the gap * This is computed based on CSS rule used in the editor: * For block with width set to X percent * width: calc(X% - (var(--wp--style--block-gap) * (100 - X)/100))); * * @param float $block_width Block width in pixels. * @param float $flex_gap Flex gap in pixels. * @param float $block_width_percent Block width in percent. * @return int */ private function get_width_without_gap( float $block_width, float $flex_gap, float $block_width_percent ): int { $width_gap_reduction = $flex_gap * ( ( 100 - $block_width_percent ) / 100 ); return intval( floor( $block_width - $width_gap_reduction ) ); } }