Layout.php
329 lines
| 1 | <?php |
| 2 | /** |
| 3 | * A helper class for rendering an individual Flexible Content Layout. |
| 4 | * |
| 5 | * @package SCF |
| 6 | */ |
| 7 | |
| 8 | namespace SCF\Fields\FlexibleContent; |
| 9 | |
| 10 | // Exit if accessed directly. |
| 11 | defined( 'ABSPATH' ) || exit; |
| 12 | |
| 13 | /** |
| 14 | * Layout class for handling individual Flexible Content layouts. |
| 15 | * |
| 16 | * This class handles rendering and management of individual layouts within a Flexible Content field. |
| 17 | * It provides methods for constructing, rendering, and manipulating layouts. |
| 18 | * |
| 19 | * @since ACF 6.5 |
| 20 | */ |
| 21 | class Layout { |
| 22 | /** |
| 23 | * The Flexible Content field the layout belongs to. |
| 24 | * |
| 25 | * @var array |
| 26 | */ |
| 27 | private $field; |
| 28 | |
| 29 | /** |
| 30 | * The layout being rendered. |
| 31 | * |
| 32 | * @var array |
| 33 | */ |
| 34 | private $layout; |
| 35 | |
| 36 | /** |
| 37 | * The order of the layout. |
| 38 | * |
| 39 | * @var integer|string |
| 40 | */ |
| 41 | private $order; |
| 42 | |
| 43 | /** |
| 44 | * The value of the layout. |
| 45 | * |
| 46 | * @var mixed |
| 47 | */ |
| 48 | private $value; |
| 49 | |
| 50 | /** |
| 51 | * The input prefix. |
| 52 | * |
| 53 | * @var string |
| 54 | */ |
| 55 | private $prefix; |
| 56 | |
| 57 | /** |
| 58 | * If the layout is disabled. |
| 59 | * |
| 60 | * @var boolean |
| 61 | */ |
| 62 | private $disabled; |
| 63 | |
| 64 | /** |
| 65 | * If the layout has been renamed, the new name of the layout. |
| 66 | * |
| 67 | * @var string |
| 68 | */ |
| 69 | private $renamed; |
| 70 | |
| 71 | /** |
| 72 | * Constructs the class. |
| 73 | * |
| 74 | * @since 6.5 |
| 75 | * |
| 76 | * @param array $field The Flexible Content field the layout belongs to. |
| 77 | * @param array $layout The layout to render. |
| 78 | * @param integer|string $order The order of the layout. |
| 79 | * @param mixed $value The value of the layout. |
| 80 | * @param boolean $disabled If the layout is disabled. |
| 81 | * @param string $renamed If the layout has been renamed, the new name of the layout. |
| 82 | */ |
| 83 | public function __construct( $field, $layout, $order, $value, $disabled = false, $renamed = '' ) { |
| 84 | $this->field = $field; |
| 85 | $this->layout = $layout; |
| 86 | $this->order = $order; |
| 87 | $this->value = $value; |
| 88 | $this->disabled = $disabled; |
| 89 | $this->renamed = $renamed; |
| 90 | } |
| 91 | |
| 92 | /** |
| 93 | * Renders the layout. |
| 94 | * |
| 95 | * @since 6.5 |
| 96 | * |
| 97 | * @return void |
| 98 | */ |
| 99 | public function render() { |
| 100 | $id = 'row-' . $this->order; |
| 101 | $class = 'layout'; |
| 102 | |
| 103 | if ( 'acfcloneindex' === $this->order ) { |
| 104 | $id = 'acfcloneindex'; |
| 105 | $class .= ' acf-clone'; |
| 106 | } |
| 107 | |
| 108 | $this->prefix = $this->field['name'] . '[' . $id . ']'; |
| 109 | |
| 110 | $div_attrs = array( |
| 111 | 'class' => $class, |
| 112 | 'data-id' => $id, |
| 113 | 'data-layout' => $this->layout['name'], |
| 114 | 'data-label' => $this->layout['label'], |
| 115 | 'data-min' => $this->layout['min'], |
| 116 | 'data-max' => $this->layout['max'], |
| 117 | 'data-enabled' => $this->disabled ? 0 : 1, |
| 118 | 'data-renamed' => empty( $this->renamed ) ? 0 : 1, |
| 119 | ); |
| 120 | |
| 121 | echo '<div ' . acf_esc_attrs( $div_attrs ) . '>'; // Layout wrapper div. |
| 122 | |
| 123 | acf_hidden_input( |
| 124 | array( |
| 125 | 'name' => $this->prefix . '[acf_fc_layout]', |
| 126 | 'value' => $this->layout['name'], |
| 127 | ) |
| 128 | ); |
| 129 | |
| 130 | acf_hidden_input( |
| 131 | array( |
| 132 | 'class' => 'acf-fc-layout-disabled', |
| 133 | 'name' => $this->prefix . '[acf_fc_layout_disabled]', |
| 134 | 'value' => $this->disabled ? 1 : 0, |
| 135 | ) |
| 136 | ); |
| 137 | |
| 138 | acf_hidden_input( |
| 139 | array( |
| 140 | 'class' => 'acf-fc-layout-custom-label', |
| 141 | 'name' => $this->prefix . '[acf_fc_layout_custom_label]', |
| 142 | 'value' => $this->renamed, |
| 143 | ) |
| 144 | ); |
| 145 | |
| 146 | $this->action_buttons(); |
| 147 | |
| 148 | if ( ! empty( $this->layout['sub_fields'] ) ) { |
| 149 | if ( 'table' === $this->layout['display'] ) { |
| 150 | $this->render_as_table(); |
| 151 | } else { |
| 152 | $this->render_as_div(); |
| 153 | } |
| 154 | } |
| 155 | |
| 156 | echo '</div>'; // End layout wrapper div. |
| 157 | } |
| 158 | |
| 159 | /** |
| 160 | * Renders a layout as a table. |
| 161 | * |
| 162 | * @since 6.5 |
| 163 | * |
| 164 | * @return void |
| 165 | */ |
| 166 | private function render_as_table() { |
| 167 | $sub_fields = $this->layout['sub_fields']; |
| 168 | ?> |
| 169 | <table class="acf-table"> |
| 170 | <thead> |
| 171 | <tr> |
| 172 | <?php |
| 173 | foreach ( $sub_fields as $sub_field ) { |
| 174 | // Set prefix to generate correct "for" attribute on <label>. |
| 175 | $sub_field['prefix'] = $this->prefix; |
| 176 | |
| 177 | // Prepare field (allow sub fields to be removed). |
| 178 | $sub_field = acf_prepare_field( $sub_field ); |
| 179 | if ( ! $sub_field ) { |
| 180 | continue; |
| 181 | } |
| 182 | |
| 183 | $th_attrs = array( |
| 184 | 'class' => 'acf-th', |
| 185 | 'data-name' => $sub_field['_name'], |
| 186 | 'data-type' => $sub_field['type'], |
| 187 | 'data-key' => $sub_field['key'], |
| 188 | ); |
| 189 | |
| 190 | if ( $sub_field['wrapper']['width'] ) { |
| 191 | $th_attrs['data-width'] = $sub_field['wrapper']['width']; |
| 192 | $th_attrs['style'] = 'width: ' . $sub_field['wrapper']['width'] . '%;'; |
| 193 | } |
| 194 | |
| 195 | echo '<th ' . acf_esc_attrs( $th_attrs ) . '>'; |
| 196 | acf_render_field_label( $sub_field ); |
| 197 | acf_render_field_instructions( $sub_field ); |
| 198 | echo '</th>'; |
| 199 | } |
| 200 | ?> |
| 201 | </tr> |
| 202 | </thead> |
| 203 | <tbody> |
| 204 | <tr><?php $this->sub_fields(); ?></tr> |
| 205 | </tbody> |
| 206 | </table> |
| 207 | <?php |
| 208 | } |
| 209 | |
| 210 | /** |
| 211 | * Renders a layout as a div. |
| 212 | * |
| 213 | * @since 6.5 |
| 214 | * |
| 215 | * @return void |
| 216 | */ |
| 217 | private function render_as_div() { |
| 218 | $class = 'acf-fields'; |
| 219 | |
| 220 | if ( 'row' === $this->layout['display'] ) { |
| 221 | $class .= ' -left'; |
| 222 | } |
| 223 | |
| 224 | echo '<div class="' . esc_attr( $class ) . '">'; |
| 225 | $this->sub_fields(); |
| 226 | echo '</div>'; |
| 227 | } |
| 228 | |
| 229 | /** |
| 230 | * Renders the layout actions (Add, Duplicate, Rename). |
| 231 | * |
| 232 | * @since 6.5 |
| 233 | * |
| 234 | * @return void |
| 235 | */ |
| 236 | private function action_buttons() { |
| 237 | $title = $this->get_title(); |
| 238 | $order = is_numeric( $this->order ) ? $this->order + 1 : 0; |
| 239 | ?> |
| 240 | <div class="acf-fc-layout-actions-wrap"> |
| 241 | <div class="acf-fc-layout-handle" title="<?php esc_attr_e( 'Drag to reorder', 'secure-custom-fields' ); ?>" data-name="collapse-layout"> |
| 242 | <span class="acf-fc-layout-order"><?php echo (int) $order; ?></span> |
| 243 | <span class="acf-fc-layout-draggable-icon"></span> |
| 244 | <span class="acf-fc-layout-title"> |
| 245 | <?php echo ! empty( $this->renamed ) ? esc_html( $this->renamed ) : $title; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped earlier in function. ?> |
| 246 | </span> |
| 247 | <span class="acf-fc-layout-original-title"> |
| 248 | (<?php echo $title; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped earlier in function. ?>) |
| 249 | </span> |
| 250 | <span class="acf-layout-disabled"><?php esc_html_e( 'Disabled', 'secure-custom-fields' ); ?></span> |
| 251 | </div> |
| 252 | <div class="acf-fc-layout-controls"> |
| 253 | <a class="acf-js-tooltip" href="#" data-name="add-layout" data-context="layout" title="<?php esc_attr( $this->field['button_label'] ); ?>"><span class="acf-icon -plus-alt "></span></a> |
| 254 | <a class="acf-js-tooltip" href="#" data-name="duplicate-layout" title="<?php esc_attr_e( 'Duplicate', 'secure-custom-fields' ); ?>"><span class="acf-icon -duplicate-alt"></span></a> |
| 255 | <a class="acf-js-tooltip" href="#" data-name="remove-layout" title="<?php esc_attr_e( 'Delete', 'secure-custom-fields' ); ?>"><span class="acf-icon -trash-alt"></span></a> |
| 256 | <a class="acf-js-tooltip" aria-haspopup="menu" href="#" data-name="more-layout-actions" title="<?php esc_attr_e( 'More layout actions...', 'secure-custom-fields' ); ?>"><span class="acf-icon -more-actions"></span></a> |
| 257 | <div class="acf-layout-collapse"> |
| 258 | <a class="acf-icon -collapse -clear" href="#" data-name="collapse-layout" aria-label="<?php esc_attr_e( 'Toggle layout', 'secure-custom-fields' ); ?>"></a> |
| 259 | </div> |
| 260 | </div> |
| 261 | </div> |
| 262 | <?php |
| 263 | } |
| 264 | |
| 265 | /** |
| 266 | * Renders the subfields for a layout. |
| 267 | * |
| 268 | * @since 6.5 |
| 269 | * @return void |
| 270 | */ |
| 271 | private function sub_fields() { |
| 272 | foreach ( $this->layout['sub_fields'] as $sub_field ) { |
| 273 | |
| 274 | // add value |
| 275 | if ( isset( $this->value[ $sub_field['key'] ] ) ) { |
| 276 | |
| 277 | // this is a normal value |
| 278 | $sub_field['value'] = $this->value[ $sub_field['key'] ]; |
| 279 | } elseif ( isset( $sub_field['default_value'] ) ) { |
| 280 | |
| 281 | // no value, but this sub field has a default value |
| 282 | $sub_field['value'] = $sub_field['default_value']; |
| 283 | } |
| 284 | |
| 285 | // update prefix to allow for nested values |
| 286 | $sub_field['prefix'] = $this->prefix; |
| 287 | |
| 288 | // Render the input. |
| 289 | $el = 'table' === $this->layout['display'] ? 'td' : 'div'; |
| 290 | acf_render_field_wrap( $sub_field, $el ); |
| 291 | } |
| 292 | } |
| 293 | |
| 294 | /** |
| 295 | * Returns the filtered layout title. |
| 296 | * |
| 297 | * @since 6.5 |
| 298 | * |
| 299 | * @return string |
| 300 | */ |
| 301 | public function get_title() { |
| 302 | $rows = array(); |
| 303 | $rows[ $this->order ] = $this->value; |
| 304 | |
| 305 | acf_add_loop( |
| 306 | array( |
| 307 | 'selector' => $this->field['name'], |
| 308 | 'name' => $this->field['name'], |
| 309 | 'value' => $rows, |
| 310 | 'field' => $this->field, |
| 311 | 'i' => $this->order, |
| 312 | 'post_id' => 0, |
| 313 | ) |
| 314 | ); |
| 315 | |
| 316 | // Make the title filterable. |
| 317 | $title = esc_html( $this->layout['label'] ); |
| 318 | $title = apply_filters( 'acf/fields/flexible_content/layout_title', $title, $this->field, $this->layout, $this->order ); |
| 319 | $title = apply_filters( 'acf/fields/flexible_content/layout_title/name=' . $this->field['_name'], $title, $this->field, $this->layout, $this->order ); |
| 320 | $title = apply_filters( 'acf/fields/flexible_content/layout_title/key=' . $this->field['key'], $title, $this->field, $this->layout, $this->order ); |
| 321 | $title = acf_esc_html( $title ); |
| 322 | |
| 323 | acf_remove_loop(); |
| 324 | reset_rows(); // TODO: Make sure this is actually where this should go if needed at all. |
| 325 | |
| 326 | return $title; |
| 327 | } |
| 328 | } |
| 329 |