Hi,
According to the “isssue” described here, I’m trying to create Custom Element named Unwrapped Loop.
Goal is to remove loop wrapper created by bricks on each ACF Flexible Content or Repeater field loop iteration .
Looks like the better way is :
- Clone original includes/elements/container.php element to elements/unwrapped-loop.php in my child theme
- Edit it renaming some stuff (className, $name, $category, label(), etc)
- Remove unnecessary controls (except loop)
- Remove loop controls condition based on element name (only div, container, block and section are allowed by default)
- Add custom notice for experimental disclaimer purpose
- in render() function, comment div wrapping
- Call unwrapped-loop.php in child theme using function.php (doc)
Looks like it works like a charm with flexible content, still testing with differents context to improve it.
Here’s the temporary code. I’ll update it during my research. If you are interested by helping me,
you are welcome
<?php
use Bricks\Breakpoints;
use Bricks\Capabilities;
use Bricks\Database;
use Bricks\Frontend;
use Bricks\Helpers;
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
class Element_Unwrapped_Loop extends \Bricks\Element {
/**
* How to create custom elements in Bricks
*
* https://academy.bricksbuilder.io/article/create-your-own-elements
*/
public $category = 'custom';
public $name = 'unwrapped-loop';
public $icon = 'fas fa-infinity'; // FontAwesome 5 icon in builder (https://fontawesome.com/icons)
public $tag = 'no-tag';
public $vue_component = 'bricks-nestable';
public $nestable = true;
public function get_label() {
return esc_html__( 'Unwrapped loop', 'bricks' );
}
public function get_keywords() {
return ['loop', 'query'];
}
public function set_controls() {
$this->controls['infoNoAccess'] = [
'type' => 'info',
'content' => esc_html__( 'This experimental Unwrapped Loop allows you to virtually wrap loop avoiding Bricks creates DOM element arround each loop iteration. Code based on original container.php element.', 'bricks' ),
];
$this->controls['loopSeparator'] = [
'type' => 'separator',
];
/**
* Loop Builder
*
* Enable for elements: Container, Block, Div and Section (@since 1.8)
*/
$this->controls = array_replace_recursive( $this->controls, $this->get_loop_builder_controls() );
}
/**
* Return shape divider HTML
*/
public static function get_shape_divider_html( $settings = [] ) {
$shape_dividers = ! empty( $settings['_shapeDividers'] ) && is_array( $settings['_shapeDividers'] ) ? $settings['_shapeDividers'] : [];
$output = '';
foreach ( $shape_dividers as $shape ) {
$shape_name = ! empty( $shape['shape'] ) ? $shape['shape'] : false;
// Skip: No shape set
if ( ! $shape_name ) {
continue;
}
$svg = '';
// Custom shape from attachment ID (@since 1.8.6)
if ( $shape_name === 'custom' ) {
$svg_path = ! empty( $shape['shapeCustom']['id'] ) ? get_attached_file( $shape['shapeCustom']['id'] ) : false;
$svg = $svg_path ? Helpers::file_get_contents( $svg_path ) : false;
}
// Shape from file
else {
$svg = Helpers::file_get_contents( BRICKS_PATH_ASSETS . "svg/shapes/{$shape_name}.svg" );
}
// Skip: SVG file doesn't exist
if ( ! $svg ) {
continue;
}
$shape_classes = [ 'bricks-shape-divider' ];
$shape_styles = [];
// Shape classes
if ( isset( $shape['front'] ) ) {
$shape_classes[] = 'front';
}
if ( isset( $shape['flipHorizontal'] ) ) {
$shape_classes[] = 'flip-horizontal';
}
if ( isset( $shape['flipVertical'] ) ) {
$shape_classes[] = 'flip-vertical';
}
if ( isset( $shape['overflow'] ) ) {
$shape_classes[] = 'overflow';
}
// Shape styles
if ( isset( $shape['horizontalAlign'] ) ) {
$shape_styles[] = "justify-content: {$shape['horizontalAlign']}";
}
if ( isset( $shape['verticalAlign'] ) ) {
$shape_styles[] = "align-items: {$shape['verticalAlign']}";
}
// Shape inner styles
$shape_inner_styles = [];
$shape_css_properties = [
'height',
'width',
'top',
'right',
'bottom',
'left',
];
foreach ( $shape_css_properties as $property ) {
$value = isset( $shape[ $property ] ) ? $shape[ $property ] : null;
if ( $value !== null ) {
// Append default unit
if ( is_numeric( $value ) ) {
$value .= 'px';
}
$shape_inner_styles[] = "{$property}: {$value}";
}
}
if ( isset( $shape['rotate'] ) ) {
$rotate = intval( $shape['rotate'] );
$shape_inner_styles[] = "transform: rotate({$rotate}deg)";
}
$output .= '<div class="' . join( ' ', $shape_classes ) . '" style="' . join( '; ', $shape_styles ) . '">';
$output .= '<div class="bricks-shape-divider-inner" style="' . join( '; ', $shape_inner_styles ) . '">';
$dom = new \DOMDocument();
libxml_use_internal_errors( true );
$dom->loadXML( $svg );
libxml_clear_errors();
// SVG styles
$svg_styles = [];
if ( isset( $shape['fill']['raw'] ) ) {
$svg_styles[] = "fill: {$shape['fill']['raw']}";
} elseif ( isset( $shape['fill']['rgb'] ) ) {
$svg_styles[] = "fill: {$shape['fill']['rgb']}";
} elseif ( isset( $shape['fill']['hex'] ) ) {
$svg_styles[] = "fill: {$shape['fill']['hex']}";
}
foreach ( $dom->getElementsByTagName( 'svg' ) as $element ) {
$element->setAttribute( 'style', join( '; ', $svg_styles ) );
}
$svg = $dom->saveXML();
$output .= str_replace( '<?xml version="1.0"?>', '', $svg );
$output .= '</div>';
$output .= '</div>';
}
return $output;
}
/**
* Return background video HTML
*/
public function get_background_video_html( $settings ) {
// Loop over all breakpoints
foreach ( Breakpoints::$breakpoints as $breakpoint ) {
$setting_key = $breakpoint['key'] === 'desktop' ? '_background' : "_background:{$breakpoint['key']}";
$background = ! empty( $settings[ $setting_key ] ) ? $settings[ $setting_key ] : false;
$video_url = ! empty( $background['videoUrl'] ) ? $background['videoUrl'] : false;
$video_attributes = [];
if ( strpos( $video_url, '{' ) !== false ) {
$video_url = bricks_render_dynamic_data( $video_url, $this->post_id, 'link' );
}
if ( $video_url ) {
$attributes[] = 'class="bricks-background-video-wrapper bricks-lazy-video"';
$attributes[] = 'data-background-video-url="' . esc_url( $video_url ) . '"';
if ( ! empty( $background['videoScale'] ) ) {
$attributes[] = 'data-background-video-scale="' . $background['videoScale'] . '"';
}
if ( ! empty( $background['videoAspectRatio'] ) ) {
$attributes[] = 'data-background-video-ratio="' . $background['videoAspectRatio'] . '"';
}
if ( ! empty( $background['videoStartTime'] ) ) {
$attributes[] = 'data-background-video-start="' . $background['videoStartTime'] . '"';
}
if ( ! empty( $background['videoEndTime'] ) ) {
$attributes[] = 'data-background-video-end="' . $background['videoEndTime'] . '"';
}
if ( empty( $background['videoPlayOnce'] ) ) {
$attributes[] = 'data-background-video-loop="1"';
}
if ( ! empty( $background['videoShowAtBreakpoint'] ) ) {
$breakpoint = Breakpoints::get_breakpoint_by( 'key', $background['videoShowAtBreakpoint'] );
$width = isset( $breakpoint['width'] ) ? $breakpoint['width'] : null;
// Is base breakpoint
if ( isset( $breakpoint['base'] ) ) {
$breakpoints = Breakpoints::$breakpoints;
foreach ( $breakpoints as $index => $bp ) {
// Is first breakpoint
if ( $bp['key'] === $breakpoint['key'] && $index === 0 ) {
// Get 'width' of next breakpoint
$next_breakpoint = isset( $breakpoints[ $index + 1 ] ) ? $breakpoints[ $index + 1 ] : null;
if ( $next_breakpoint ) {
$width = Breakpoints::$is_mobile_first ? 0 : $next_breakpoint['width'] + 1;
}
}
}
}
if ( $width ) {
$attributes[] = 'data-background-video-show-at-breakpoint="' . $width . '"';
}
}
// Video poster (@since 1.11)
if ( ! empty( $background['videoPoster'] ) ) {
$video_attributes[] = 'poster="' . $background['videoPoster']['url'] . '"';
$attributes[] = 'data-background-video-poster="' . $background['videoPoster']['url'] . '"';
}
// YouTube video poster (@since 1.11)
if ( ! empty( $background['videoPosterYouTube'] ) ) {
$youtube_poster_size = $background['videoPosterYouTubeSize'] ?? 'maxresdefault';
$attributes[] = 'data-background-video-poster-yt-size="' . $youtube_poster_size . '"';
}
$attributes = join( ' ', $attributes );
$video_attributes = join( ' ', $video_attributes );
// @since 1.4: Chrome doesn't play the .mp4 background video if the <video> tag is injected programmatically using JavaScript
return "<div $attributes><video autoplay loop playsinline muted $video_attributes></video></div>";
}
}
}
public function render() {
$element = $this->element;
$settings = $this->settings ?? [];
$output = '';
// Bricks Query Loop
if ( isset( $settings['hasLoop'] ) ) {
// Hold the component to first unset 'hasLoop' and then add back 'hasLoop' after the query->render (@since 1.12)
$original_component = Helpers::get_component( $element );
// Hold the global element to first unset 'hasLoop' and then add back 'hasLoop' after the query->render
$global_element = Helpers::get_global_element( $element );
// STEP: Query
add_filter( 'bricks/posts/query_vars', [ $this, 'maybe_set_preview_query' ], 10, 3 );
// Is component: Generate random ID for component instance (@since 1.12)
if ( ! empty( $element['instanceId'] ) && ! empty( $element['parentComponent'] ) ) {
$element['id'] .= ':' . $element['instanceId'];
}
$query = new \Bricks\Query( $element );
remove_filter( 'bricks/posts/query_vars', [ $this, 'maybe_set_preview_query' ], 10, 3 );
// Prevent endless loop
unset( $element['settings']['hasLoop'] );
// Prevent endless loop for component (@since 1.12)
if ( $original_component ) {
// Find all component element and unset 'hasLoop'
Database::$global_data['components'] = array_map(
function( $component ) use ( $element ) {
if ( ! empty( $element['cid'] ) && $element['cid'] === $component['id'] ) {
foreach ( $component['elements'] as $index => $component_element ) {
if ( isset( $component['elements'][ $index ]['settings']['hasLoop'] ) ) {
unset( $component['elements'][ $index ]['settings']['hasLoop'] );
}
}
}
return $component;
},
Database::$global_data['components']
);
}
// Prevent endless loop for global element
if ( ! empty( $global_element['global'] ) ) {
// Find the global element and unset 'hasLoop'
Database::$global_data['elements'] = array_map(
function( $global_element ) use ( $element ) {
if ( ! empty( $element['global'] ) && $element['global'] === $global_element['global'] ) {
unset( $global_element['settings']['hasLoop'] );
}
return $global_element;
},
Database::$global_data['elements']
);
}
// STEP: Render loop
$output = $query->render( 'Bricks\Frontend::render_element', compact( 'element' ) );
echo $output;
// Prevent endless loop for component (@since 1.12)
if ( $original_component ) {
// Restore orignal component with 'hasLoop' setting after execute render_element
Database::$global_data['components'] = array_map(
function( $component ) use ( $element, $original_component ) {
if ( ! empty( $element['cid'] ) && $element['cid'] === $component['id'] ) {
$component = $original_component;
}
return $component;
},
Database::$global_data['components']
);
}
// Prevent endless loop for global element
if ( ! empty( $global_element['global'] ) ) {
// Add back global element 'hasLoop' setting after execute render_element
Database::$global_data['elements'] = array_map(
function( $global_element ) use ( $element ) {
if ( ! empty( $element['global'] ) && $element['global'] === $global_element['global'] ) {
$global_element['settings']['hasLoop'] = true;
}
return $global_element;
},
Database::$global_data['elements']
);
}
// STEP: Infinite scroll
$this->render_query_loop_trail( $query );
// Destroy Query to explicitly remove it from global store
$query->destroy();
unset( $query );
return;
}
// Render the video wrapper first so we know it before adding the has-bg-video class
$video_wrapper_html = $this->get_background_video_html( $settings );
// No background video set on element ID: Loop over element global classes
if ( ! $video_wrapper_html ) {
$elements_class_ids = ! empty( $settings['_cssGlobalClasses'] ) ? $settings['_cssGlobalClasses'] : [];
if ( count( $elements_class_ids ) ) {
$global_classes = Database::$global_data['globalClasses'];
foreach ( $global_classes as $global_class ) {
$global_class_id = ! empty( $global_class['id'] ) ? $global_class['id'] : '';
if ( ! $video_wrapper_html && in_array( $global_class_id, $elements_class_ids ) ) {
if ( ! empty( $global_class['settings'] ) ) {
$video_wrapper_html = $this->get_background_video_html( $global_class['settings'] );
}
}
}
}
}
// Add .has-bg-video to set z-index: 1 (#2g9ge90)
if ( ! empty( $video_wrapper_html ) ) {
$this->set_attribute( '_root', 'class', 'has-bg-video' );
}
// Add .has-shape to set position: relative (#2t7w2bq)
if ( ! empty( $settings['_shapeDividers'] ) ) {
$this->set_attribute( '_root', 'class', 'has-shape' );
}
// Non-megamenu dropdown content: Set tag to 'ul'
$parent_id = ! empty( $element['parent'] ) ? $element['parent'] : false;
$parent_element = ! empty( Frontend::$elements[ $parent_id ] ) ? Frontend::$elements[ $parent_id ] : false;
if ( $parent_element && $parent_element['name'] === 'dropdown' && ! isset( $parent_element['settings']['megaMenu'] ) ) {
$this->tag = 'ul';
}
/**
* Live search wrapper
*
* Add 'data-brx-ls-wrapper' to hide live search wrapper on page load.
*
* @since 1.9.6
*/
if ( count( Frontend::$live_search_wrapper_selectors ) ) {
foreach ( Frontend::$live_search_wrapper_selectors as $live_search_query_id => $live_search_wrapper_selector ) {
/**
* 1. Last six-characters of live search results selector match element.id
* 2. Live search results selector matches custom element ID
*/
$match_default_id = "#brxe-{$element['id']}" === $live_search_wrapper_selector;
$match_custom_id = ! empty( $element['settings']['_cssId'] ) && "#{$element['settings']['_cssId']}" === $live_search_wrapper_selector;
if ( $match_default_id || $match_custom_id ) {
unset( Frontend::$live_search_wrapper_selectors[ $live_search_query_id ] );
$this->set_attribute( '_root', 'data-brx-ls-wrapper', $live_search_query_id );
// Ensure setting element 'id' to target the live search wrapper with CSS. Could be omittied, if the elment doesn't has_css_settings.
if ( empty( $this->attributes['_root']['id'] ) ) {
$this->set_attribute( '_root', 'id', $this->get_element_attribute_id() );
}
}
}
}
// Default: Non-query loop
//$output .= "<{$this->tag} {$this->render_attributes( '_root' )}>";
$output .= self::get_shape_divider_html( $settings );
$output .= $video_wrapper_html;
// Render element children
if ( ! empty( $element['children'] ) && is_array( $element['children'] ) ) {
foreach ( $element['children'] as $child_id ) {
$child_element = Frontend::$elements[ $child_id ] ?? false;
/**
* Skip element: Component with this 'cid' doesn't exist in database
*
* @since 1.12
*/
if ( ! empty( $child_element['cid'] ) && ! Helpers::get_component_by_cid( $child_element['cid'] ) ) {
continue;
}
$child_html = $child_element ? Frontend::render_element( $child_element ) : false; // Recursive
if ( $child_element && $child_html ) {
// Nav items is parent element: Wrap this nav link in <li> (@since 1.8)
$parent_id = $child_element['parent'];
$parent_element = ! empty( Frontend::$elements[ $parent_id ] ) ? Frontend::$elements[ $parent_id ] : false;
$inside_nav_items = ! empty( $parent_element['settings']['_hidden']['_cssClasses'] ) ? $parent_element['settings']['_hidden']['_cssClasses'] === 'brx-nav-nested-items' : false;
$inside_dropdown_content = ! empty( $parent_element['settings']['_hidden']['_cssClasses'] ) ? $parent_element['settings']['_hidden']['_cssClasses'] === 'brx-dropdown-content' : false;
// Wrap in <li> if child HTML does not start with an 'li' tag (e.g. non-megamenu dropdown)
if (
( $inside_nav_items || $inside_dropdown_content ) &&
( strpos( $child_html, '<li' ) === false || strpos( $child_html, '<li' ) !== 0 )
) {
$dropdown_id = $parent_element['parent'];
$dropdown_element = ! empty( Frontend::$elements[ $dropdown_id ] ) ? Frontend::$elements[ $dropdown_id ] : false;
// Megamenu: Don't wrap dropdown item in <li>
if ( isset( $dropdown_element['settings']['megaMenu'] ) ) {
$output .= $child_html;
}
// Default: Wrap menu item in <li>
else {
if ( isset( $child_element['settings']['hasLoop'] ) ) {
// Get first HTML tag
preg_match( '/<([a-zA-Z]+)([^>]*)>/', $child_html, $matches );
$html_tag = $matches[1] ?? 'div';
// Wrap each loop node in <li>
$output .= preg_replace( '/(<' . $html_tag . '.*?>.*?<\/' . $html_tag . '>)/', '<li class="menu-item">$1</li>', $child_html );
} else {
$output .= '<li class="menu-item">';
$output .= $child_html;
$output .= '</li>';
}
}
}
// Default: Render child element HTML
else {
$output .= $child_html;
}
}
}
}
/**
* STEP: Add masonry trail nodes
*
* Suppose add these nodes inside base.php but no perfect hook yet.
* Any custom element has to run this method manually in the render method.
*
* @since 1.11.1
*/
$output .= $this->maybe_masonry_trail_nodes();
//$output .= "</{$this->tag}>";
echo $output;
}
}