├── README.md ├── assets ├── css │ └── cmb2-flexible.css └── js │ └── cmb2-flexible.js ├── cmb2-flexible-content-field.php ├── composer.json └── screenshot-1.png /README.md: -------------------------------------------------------------------------------- 1 | # CMB2 Flexible Field 2 | 3 | The CMB2 Flexible Field allows the creation of group layouts, which can then be selected and switched between by a user. Each layout group contains a set of existing CMB2 fields, defined in an array in the field setup. These layout groups are then toggleable, sortable and repeatable. 4 | 5 | ![Screenshot of UI](screenshot-1.png) 6 | 7 | The data itself is saved to the field's ID in a serialized and iterable array with the layout group ID attached. 8 | 9 | 10 | ## Adding a Flexible Field 11 | 12 | Adding a flexible field is the same as adding any other CMB2 field. The only addition is the `layouts` option, which should contain an array of arrays, each with a separate group. 13 | 14 | To begin, set up a standard CMB2 metabox: 15 | 16 | ``` 17 | // Basic CMB2 Metabox declaration 18 | $cmb = new_cmb2_box( array( 19 | 'id' => 'prefix-metabox-id', 20 | 'title' => __( 'Flexible Content Test' ), 21 | 'object_types' => array( 'post', ), 22 | ) ); 23 | 24 | Then add your flexible field definition. Each layout group should be defined in the layouts array, with the `ID` for that group as its key. Each layout group can contain a `title` and a list of CMB2 `fields`. 25 | 26 | // Sample Flexible Field 27 | $cmb->add_field( array( 28 | 'name' => __( 'Test Flexible', 'cmb2-flexible' ), 29 | 'desc' => __( 'field description (optional)', 'cmb2-flexible' ), 30 | 'id' => 'prefix_flexible', 31 | 'type' => 'flexible', 32 | 'layouts' => array( 33 | 'text' => array( 34 | 'title' => 'Text Group', 35 | 'fields' => array( 36 | array( 37 | 'type' => 'text', 38 | 'name' => 'Title for Text Group', 39 | 'id' => 'title', 40 | ), 41 | array( 42 | 'type' => 'textarea', 43 | 'name' => 'Description for Text Group', 44 | 'id' => 'description', 45 | ) 46 | ), 47 | ), 48 | 'image' => array( 49 | 'title' => 'Image Group', 50 | 'fields' => array( 51 | array( 52 | 'type' => 'file', 53 | 'name' => 'Image for Image Group', 54 | 'id' => 'title', 55 | ), 56 | array( 57 | 'type' => 'textarea', 58 | 'name' => 'Description for Image Group', 59 | 'id' => 'description', 60 | ) 61 | ), 62 | ), 63 | ) 64 | ) ); 65 | ``` 66 | 67 | ## Getting Data from a Flexible Field 68 | Flexible fields are stored in a single meta key, and can be retrieved using the WordPress API. This will return an array of data, each with the layout key defined as `layout` and the group's data by field ID. 69 | 70 | ``` 71 | $flexible_fields = get_post_meta( $post_id, 'flexible_field_name', true ); 72 | foreach( $flexible_fields as $field ) { 73 | if ( 'text' === $field['layout'] ) { ?> 74 |

75 | .cmb-th, .cmb-type-flexible > .cmb-td { 2 | float: none !important; 3 | width: 100% !important; 4 | } 5 | 6 | .cmb-type-flexible > .cmb-th { 7 | font-size: 1.2em; 8 | padding-bottom: 1em; 9 | font-weight: normal; 10 | } 11 | 12 | .cmb-type-flexible .cmb-group-title { 13 | background-color: #e9e9e9; 14 | } 15 | 16 | 17 | 18 | .cmb-flexible-add { 19 | position: relative; 20 | } 21 | 22 | .cmb-flexible-add-list { 23 | position: absolute; 24 | bottom: 150%; 25 | left: 0; 26 | list-style-type: none; 27 | background: #333; 28 | padding: 0; 29 | text-align: center; 30 | border-radius: 3px; 31 | } 32 | 33 | .cmb-flexible-add-list.hidden { 34 | display: none; 35 | } 36 | 37 | .cmb-flexible-add-list:before { 38 | content: ''; 39 | position: absolute; 40 | bottom: -5px; 41 | left: 50%; 42 | width: 0; 43 | height: 0; 44 | margin-left: -5px; 45 | border-color: transparent; 46 | border-style: solid; 47 | border-width: 5px 5px 0; 48 | border-top-color: #333; 49 | } 50 | 51 | .cmb-flexible-add-list button { 52 | background: none; 53 | border: none; 54 | color: white; 55 | font-weight: bold; 56 | padding: 5px 25px; 57 | display: block; 58 | width: 100%; 59 | cursor: pointer; 60 | } 61 | 62 | .cmb-flexible-add-list button:hover { 63 | background: #777; 64 | } 65 | 66 | 67 | -------------------------------------------------------------------------------- /assets/js/cmb2-flexible.js: -------------------------------------------------------------------------------- 1 | var cmb_flexible = {}; 2 | window.CMB2 = window.CMB2 || {}; 3 | window.CMB2.wysiwyg = window.CMB2.wysiwyg || false; 4 | 5 | (function( $, cmb ) { 6 | 7 | var l10n = window.cmb2_l10 || {}; 8 | 9 | /** 10 | * Iniitalize Flexible content field 11 | */ 12 | cmb_flexible.init = function() { 13 | var $metabox = cmb.metabox(); 14 | 15 | $metabox 16 | .on( 'click', '.cmb2-add-flexible-row', cmb_flexible.addFlexibleRow ) 17 | .on( 'click', '.cmb-flexible-add-button', cmb_flexible.removeFlexibleHiddenClass ) 18 | .on( 'click', '.cmb-shift-flexible-rows', cmb_flexible.shiftRows ) 19 | .on( 'cmb2_remove_row', cmb_flexible.removeDisabledRows ) 20 | .on( 'click', '.cmb-flexible-rows .cmb-remove-group-row', cmb_flexible.removeLastRow ); 21 | 22 | $( '.cmb-flexible-wrap' ).find( '.cmb-repeatable-grouping' ).each( function() { 23 | $( this ).find( '.button.cmb-remove-group-row' ).before( ' ' ); 24 | } ); 25 | }; 26 | 27 | /** 28 | * Make sure no rows in flexible fields are set to disabled 29 | * Reset iterators whenever a flexible field is removed. 30 | * 31 | * @param {object} evt Event object. 32 | */ 33 | cmb_flexible.removeDisabledRows = function( evt ) { 34 | var $el = $( evt.target ); 35 | if ( $el.find( '.cmb-flexible-rows' ).length > 0 ) { 36 | $el.find( '.cmb-remove-group-row' ).prop( 'disabled', false ); 37 | cmb_flexible.resetIterators( $el ); 38 | } 39 | }; 40 | 41 | /** 42 | * Add the ability to remove all rows for flexible content fields. 43 | */ 44 | cmb_flexible.removeLastRow = function() { 45 | var el = $(document).find( this ); 46 | 47 | // Make sure to eliminate the final group if it exists. 48 | if ( el.length > 0 ) { 49 | var $this = $( this ); 50 | var $table = $( document.getElementById( $this.data( 'selector' ) ) ); 51 | var $parent = $this.parents( '.cmb-repeatable-grouping' ); 52 | $parent.remove(); 53 | } 54 | }; 55 | 56 | /** 57 | * Send an AJAX request for a new field and then add it to the DOM. 58 | * 59 | * Once a field has been added, need to make sure to initialize its 60 | * dependencies and WYSIWYG functionality 61 | * 62 | * @param {object} evt Event object. 63 | */ 64 | cmb_flexible.addFlexibleRow = function( evt ) { 65 | evt.preventDefault(); 66 | 67 | var $this = $( this ); 68 | var metabox = $this.closest( '.cmb2-postbox' ).attr( 'id' ); 69 | var flexible_group = $this.closest( '.cmb-repeatable-group' ); 70 | var flexible_wrap = flexible_group.find( '.cmb-flexible-rows' ).last(); 71 | var field_id = flexible_group.attr( 'data-groupid' ); 72 | var type = $this.attr( 'data-type' ); 73 | var latest = flexible_group.find('.cmb-repeatable-grouping').last(); 74 | var latest_index; 75 | 76 | $( flexible_wrap ).css( { opacity: 0.5 } ); 77 | 78 | if ( latest.length > 0 ) { 79 | latest_index = latest.attr( 'data-iterator' ); 80 | } 81 | 82 | $this.closest( '.cmb-flexible-add-list' ).addClass( 'hidden' ); 83 | 84 | $.ajax({ 85 | method: 'POST', 86 | url: ajaxurl, 87 | data: { 88 | type: type, 89 | metabox_id: metabox, 90 | field_id: field_id, 91 | latest_index: latest_index, 92 | action: 'get_flexible_content_row', 93 | cmb2_ajax_nonce: cmb2_l10.ajax_nonce, 94 | }, 95 | 96 | success: function( response ) { 97 | var el = response.data; 98 | flexible_wrap.append( el.output ); 99 | var newRow = flexible_wrap.find( '.cmb-repeatable-grouping' ).last(); 100 | cmb.newRowHousekeeping( newRow ); 101 | cmb.afterRowInsert( newRow ); 102 | $( newRow ).find( '.button.cmb-remove-group-row' ).before( ' ' ); 103 | $( newRow ).find( '.cmb2-wysiwyg-placeholder' ).each( function() { 104 | $this = $( this ); 105 | data = $this.data(); 106 | 107 | if ( data.groupid ) { 108 | 109 | data.id = $this.attr( 'id' ); 110 | data.name = $this.attr( 'name' ); 111 | data.value = $this.val(); 112 | window.CMB2.wysiwyg.init( $this, data, false ); 113 | if ( 'undefined' !== typeof window.QTags ) { 114 | window.QTags._buttonsInit(); 115 | } 116 | } 117 | } ); 118 | 119 | $( flexible_wrap ).css( { opacity: 1 } ); 120 | } 121 | }); 122 | }; 123 | 124 | /** 125 | * Show list of types on click. 126 | * 127 | * @param {object} evt Event object. 128 | */ 129 | cmb_flexible.removeFlexibleHiddenClass = function( evt ) { 130 | evt.preventDefault(); 131 | var list = $( this ).next( '.cmb-flexible-add-list' ).toggleClass( 'hidden' ); 132 | }; 133 | 134 | /** 135 | * Sort through inputs and change the name attributes depending 136 | * on new iterator number. 137 | * 138 | * @param {object} $el jQuery Element object. 139 | * @param {int} prevNum Number to look for. 140 | * @param {int} newNum Number to change to. 141 | */ 142 | cmb_flexible.updateFlexibleNames = function( $el, prevNum, newNum ) { 143 | if ( $el.length > 0 ) { 144 | newNum = newNum || prevNum - 1; 145 | $el.find( cmb.repeatEls ).each( function() { 146 | var $this = $( this ); 147 | var name = $this.attr( 'name' ); 148 | 149 | if ( typeof name !== 'undefined' ) { 150 | // We add an extra '[' (as in '][') so that sub-fields are not effected, only the parent. 151 | var $newName = name.replace( '[' + prevNum + '][', '[' + newNum + '][' ); 152 | $this.attr( 'name', $newName ); 153 | } 154 | } ); 155 | } 156 | }; 157 | 158 | /** 159 | * Shift Rows by switching them in the DOM. 160 | * 161 | * @param {object} evt Event object. 162 | */ 163 | cmb_flexible.shiftRows = function( evt ) { 164 | evt.preventDefault(); 165 | 166 | var $this = $( this ); 167 | var $from = $this.closest( '.cmb-repeatable-grouping' ); 168 | var fromNum = $from.attr( 'data-iterator' ); 169 | var direction = $this.hasClass( 'move-up' ) ? 'up' : 'down'; 170 | var $goto = 'up' === direction ? $from.prev( '.cmb-repeatable-grouping' ) : $from.next( '.cmb-repeatable-grouping' ); 171 | var gotoNum = $goto.attr( 'data-iterator' ); 172 | 173 | if ( 'up' === direction && 0 === parseInt( fromNum ) ) { 174 | return false; 175 | } 176 | 177 | $from.add($goto).find( '.wp-editor-wrap textarea' ).each( function() { 178 | window.CMB2.wysiwyg.destroy( $( this ).attr( 'id' ) ); 179 | } ); 180 | 181 | if ( 'up' === direction ) { 182 | $goto.before( $from ); 183 | } 184 | 185 | if ( 'down' === direction ) { 186 | $goto.after( $from ); 187 | } 188 | 189 | cmb_flexible.updateFlexibleNames( $from, fromNum, gotoNum ); 190 | cmb_flexible.updateFlexibleNames( $goto, gotoNum, fromNum ); 191 | 192 | cmb_flexible.resetIterators( $this ); 193 | 194 | $from.add( $goto ).each(function() { 195 | window.CMB2.wysiwyg.initRow( $( this ) ); 196 | } ); 197 | 198 | }; 199 | 200 | /** 201 | * We use both the data-iterator attribute to shift rows, 202 | * so need to make sure that is reset as well. 203 | * 204 | * @param {object} $el jQuery element. 205 | */ 206 | cmb_flexible.resetIterators = function( $el ) { 207 | var $table = $el.closest( '.cmb-repeatable-group' ); 208 | $table.find( '.cmb-repeatable-grouping' ).each( function( rowindex ) { 209 | var $row = $( this ); 210 | $row.data( 'iterator', rowindex ); 211 | $row.attr( 'data-iterator', rowindex ); 212 | } ); 213 | }; 214 | 215 | $( cmb_flexible.init ); 216 | })( jQuery, window.CMB2 ); 217 | -------------------------------------------------------------------------------- /cmb2-flexible-content-field.php: -------------------------------------------------------------------------------- 1 | init(); 37 | } 38 | return static::$instance; 39 | } 40 | 41 | /** 42 | * Add hooks and filters to flexible content field 43 | */ 44 | private function init() { 45 | add_action( 'cmb2_render_flexible', array( $this, 'render_fields' ), 10, 5 ); 46 | add_filter( 'cmb2_sanitize_flexible', array( $this, 'save_fields' ), 12, 5 ); 47 | add_filter( 'cmb2_types_esc_flexible', array( $this, 'escape_values' ), 10, 2 ); 48 | 49 | add_action( 'admin_enqueue_scripts', array( $this, 'add_scripts' ) ); 50 | add_action( 'wp_ajax_get_flexible_content_row', array( $this, 'handle_ajax' ) ); 51 | } 52 | 53 | /** 54 | * Render fields callback 55 | * 56 | * Grabs the layouts from the field data and the current data from the database, 57 | * then constructs a new field group for each used layout and renders that out, one by one. 58 | * 59 | * Groups are used so that the CMB2 API can automatically apply 60 | * necessary render functions for each individual field. 61 | * 62 | * The data for each group needs to be overrrdden though since it is stored in a different type of array. 63 | * 64 | * @param object $field Field arguments and parameters. 65 | * @param array $escaped_value Escaped value from database. 66 | * @param int $object_id Integer for full object. 67 | * @param string $object_type Object type. 68 | * @param string $field_type Field type. 69 | */ 70 | public function render_fields( $field, $escaped_value, $object_id, $object_type, $field_type ) { 71 | 72 | $metabox = $field->get_cmb(); 73 | $metabox_id = $metabox->cmb_id; 74 | $layouts = isset( $field->args['layouts'] ) ? $field->args['layouts'] : false; 75 | 76 | // Add all possible dependencies for right now. 77 | $dependencies = $this->get_dependencies( $layouts ); 78 | $field->add_js_dependencies( $dependencies ); 79 | if ( false === $layouts ) { 80 | // We need layouts for this to work. 81 | return false; 82 | } 83 | 84 | // These are the values from the fields. 85 | $data = $escaped_value; 86 | 87 | $group = $this->create_group( $field ); 88 | 89 | echo '
group_wrap_attributes( $group ), '>'; 90 | 91 | echo '
'; 92 | if ( ! empty( $data ) ) { 93 | foreach ( $data as $i => $group_details ) { 94 | $subfields = array(); 95 | $type = $group_details['layout']; 96 | 97 | $group = $this->add_subfields( $group, $metabox, $type, $i ); 98 | 99 | $metabox->render_group_row( $group, false ); 100 | } 101 | } 102 | 103 | echo '
'; 104 | 105 | echo '
'; 106 | echo ''; 107 | echo ''; 114 | echo '
'; 115 | 116 | $this->prerender_wysiwyg( $data, $layouts, $group ); 117 | 118 | echo '
'; 119 | } 120 | 121 | /** 122 | * Retrieves a list of JS dependencies based on file types. 123 | * 124 | * These are then added to the parent fields list of dependencies so that they are included at output. 125 | * 126 | * @param array $layouts List of layouts. 127 | * @return array List of dependencies 128 | */ 129 | public function get_dependencies( $layouts ) { 130 | $dependencies = array(); 131 | foreach ( $layouts as $layout ) { 132 | foreach ( $layout['fields'] as $field ) { 133 | switch ( $field['type'] ) { 134 | case 'colorpicker': 135 | wp_enqueue_style( 'wp-color-picker' ); 136 | $dependencies[] = 'wp-color-picker'; 137 | break; 138 | case 'file': 139 | case 'file_list': 140 | $dependencies[] = 'media-editor'; 141 | break; 142 | case 'text_date': 143 | case 'text_time': 144 | case 'text_datetime_timestamp': 145 | $dependencies[] = 'jquery-ui-core'; 146 | $dependencies[] = 'jquery-ui-datepicker'; 147 | break; 148 | case 'text_datetime_timestamp': 149 | case 'text_time': 150 | $dependencies[] = 'jquery-ui-datetimepicker'; 151 | break; 152 | case 'wysiwyg': 153 | $dependencies[] = 'wp-util'; 154 | $dependencies[] = 'cmb2-wysiwyg'; 155 | break; 156 | } 157 | } 158 | } 159 | 160 | if ( ! empty( $dependencies ) ) { 161 | $dependencies = array_unique( $dependencies ); 162 | } 163 | 164 | return $dependencies; 165 | } 166 | 167 | /** 168 | * Sanitization callback 169 | * 170 | * @param array $override_value Value that's being overridden. 171 | * @param array $values Value from post object. 172 | * @param int $object_id Object / field ID. 173 | * @param array $field_args Full list of arguments. 174 | * @param object $sanitizer_object Instance of CMB2_Sanitize. 175 | * @return string Data 176 | */ 177 | public function save_fields( $override_value, $values, $object_id, $field_args, $sanitizer_object ) { 178 | 179 | $flexible_field = $sanitizer_object->field; 180 | $field_id = $flexible_field->_id(); 181 | $metabox = $flexible_field->get_cmb(); 182 | $layouts = $flexible_field->args['layouts']; 183 | 184 | if ( empty( $values ) ) { 185 | return $values; 186 | } 187 | 188 | $group_args = array( 189 | 'id' => $field_id, 190 | 'type' => 'group', 191 | 'context' => 'normal', 192 | 'repeatable' => true, 193 | 'fields' => array(), 194 | ); 195 | $field_group = $flexible_field->get_field_clone( $group_args ); 196 | $field_group->data_to_save = $values; 197 | 198 | // The saved array is used to hold sanitized values. 199 | $saved = array(); 200 | foreach ( $values as $i => $group_vals ) { 201 | 202 | // Cache the type and save it in array. 203 | $type = isset( $group_vals['layout'] ) ? sanitize_key( $group_vals['layout'] ) : false; 204 | if ( ! $type ) { 205 | continue; 206 | } 207 | 208 | $saved[ $i ]['layout'] = $type; 209 | $field_group->index = $i; 210 | 211 | $layout = isset( $layouts[ $type ] ) ? $layouts[ $type ] : false; 212 | $group_args['fields'] = $layout['fields']; 213 | $field_group->set_prop( 'fields', $group_args['fields'] ); 214 | $metabox->add_field( $group_args ); 215 | foreach ( $layout['fields'] as $subfield_args ) { 216 | $sub_id = $subfield_args['id']; 217 | $field = $metabox->get_field( $subfield_args, $field_group ); 218 | $new_val = isset( $group_vals[ $sub_id ] ) ? $group_vals[ $sub_id ] : false; 219 | 220 | $new_val = $field->sanitization_cb( $new_val ); 221 | 222 | if ( is_array( $new_val ) && $field->args( 'has_supporting_data' ) ) { 223 | if ( $field->args( 'repeatable' ) ) { 224 | $_new_val = array(); 225 | foreach ( $new_val as $group_index => $grouped_data ) { 226 | // Add the supporting data to the $saved array stack. 227 | $saved[ $i ][ $grouped_data['supporting_field_id'] ][] = $grouped_data['supporting_field_value']; 228 | // Reset var to the actual value. 229 | $_new_val[ $group_index ] = $grouped_data['value']; 230 | } 231 | $new_val = $_new_val; 232 | } else { 233 | // Add the supporting data to the $saved array stack. 234 | $saved[ $i ][ $new_val['supporting_field_id'] ] = $new_val['supporting_field_value']; 235 | // Reset var to the actual value. 236 | $new_val = $new_val['value']; 237 | } 238 | } 239 | $saved[ $i ][ $sub_id ] = $new_val; 240 | } 241 | $saved[ $i ] = CMB2_Utils::filter_empty( $saved[ $i ] ); 242 | } 243 | 244 | $saved = CMB2_Utils::filter_empty( $saved ); 245 | 246 | return $saved; 247 | } 248 | 249 | 250 | /** 251 | * Add Flexible content scripts and styles 252 | */ 253 | public function add_scripts() { 254 | wp_enqueue_script( 'cmb2-flexible-content', plugin_dir_url( __FILE__ ) . 'assets/js/cmb2-flexible.js', array( 'jquery', 'cmb2-scripts' ), '0.1.1', true ); 255 | wp_enqueue_style( 'cmb2-flexible-styles', plugin_dir_url( __FILE__ ) . 'assets/css/cmb2-flexible.css', array( 'cmb2-styles' ), '0.1' ); 256 | } 257 | 258 | /** 259 | * Prerender WYSIWYG templates if necessary 260 | * 261 | * Iterates through all fields and then outputs a clone-able template if a wysiwyg field exists. 262 | * 263 | * @param array $data Data from request. 264 | * @param array $layouts Field layouts. 265 | * @param object $group Full object. 266 | */ 267 | public function prerender_wysiwyg( $data, $layouts, $group ) { 268 | $wysiwygs = array(); 269 | foreach ( $layouts as $layout ) { 270 | $fields = $layout['fields']; 271 | foreach ( $fields as $field ) { 272 | if ( 'wysiwyg' === $field['type'] ) { 273 | $wysiwygs[ $field['id'] ] = $field; 274 | } 275 | } 276 | } 277 | 278 | if ( ! empty( $data ) ) { 279 | foreach ( $data as $i => $group_details ) { 280 | $type = $group_details['layout']; 281 | foreach ( $layouts[ $type ]['fields'] as $subfield ) { 282 | if ( 'wysiwyg' === $subfield['type'] ) { 283 | unset( $wysiwygs[ $subfield['id'] ] ); 284 | } 285 | } 286 | } 287 | } 288 | 289 | $metabox = $group->get_cmb(); 290 | if ( ! empty( $wysiwygs ) ) { 291 | foreach ( $wysiwygs as $wysiwyg_id => $args ) { 292 | 293 | $group->index = 0; 294 | if ( ! $data ) { 295 | $metabox->add_field( $group->args() ); 296 | } 297 | $wysiwyg_field = $metabox->add_group_field( $group->_id(), $args ); 298 | $wysiwyg_field = $metabox->get_field( $args, $group ); 299 | $types = new CMB2_Types( $wysiwyg_field ); 300 | $wysiwyg_type = $types->get_new_render_type( 'wysiwyg', 'CMB2_Type_Wysiwyg', $args ); 301 | $wysiwyg_type->add_wysiwyg_template_for_group(); 302 | } 303 | } 304 | 305 | } 306 | 307 | /** 308 | * Handle AJAX request for a new flexible row 309 | * 310 | * Creates a new group based on a few variables, renders the output 311 | * then returns it. 312 | */ 313 | public function handle_ajax() { 314 | if ( ! ( isset( $_POST['cmb2_ajax_nonce'] ) && wp_verify_nonce( $_POST['cmb2_ajax_nonce'], 'ajax_nonce' ) ) ) { 315 | die(); 316 | } 317 | 318 | $type = isset( $_POST['type'] ) ? sanitize_key( wp_unslash( $_POST['type'] ) ) : ''; // Input var okay. 319 | $metabox_id = isset( $_POST['metabox_id'] ) ? sanitize_key( wp_unslash( $_POST['metabox_id'] ) ) : ''; // Input var okay. 320 | $field_id = isset( $_POST['field_id'] ) ? sanitize_key( wp_unslash( $_POST['field_id'] ) ) : ''; // Input var okay. 321 | $index = isset( $_POST['latest_index'] ) ? absint( $_POST['latest_index'] ) + 1 : 0; // Input var okay. 322 | 323 | $field = cmb2_get_field( $metabox_id, $field_id ); 324 | $metabox = $field->get_cmb(); 325 | $group = $this->create_group( $field ); 326 | 327 | $group = $this->add_subfields( $group, $metabox, $type, $index ); 328 | 329 | ob_start(); 330 | $metabox->render_group_row( $group, false ); 331 | $output = ob_get_clean(); 332 | 333 | wp_send_json_success( array( 334 | 'output' => $output, 335 | ) ); 336 | } 337 | 338 | /** 339 | * Create a basic group field by cloning the existing field. 340 | * 341 | * @param object $field Full field object. 342 | * @return object Cloned field object 343 | */ 344 | public function create_group( $field ) { 345 | $field_id = $field->_id(); 346 | 347 | $group_args = array( 348 | 'id' => $field_id, 349 | 'type' => 'group', 350 | 'context' => 'normal', 351 | 'repeatable' => true, 352 | 'show_names' => true, 353 | 'classes' => array( 'cmb-flexible-wrap' ), 354 | 'fields' => array(), 355 | 'options' => array(), 356 | ); 357 | 358 | $field = $field->get_field_clone( $group_args ); 359 | 360 | return $field; 361 | } 362 | 363 | /** 364 | * Add subfields to cloned field. 365 | * 366 | * @param object $field Full field object. 367 | * @param object $metabox CMB2 instance. 368 | * @param string $type Layout type. 369 | * @param int $i Field index. 370 | */ 371 | public function add_subfields( $field, $metabox, $type, $i ) { 372 | $subfields = array(); 373 | $metabox = $field->get_cmb(); 374 | $group_args = $field->args(); 375 | $layout = isset( $field->args['layouts'] ) ? $field->args['layouts'][ $type ] : false; 376 | 377 | $subfields = $layout['fields']; 378 | $subfields[] = array( 379 | 'type' => 'text', 380 | 'id' => 'layout', 381 | 'attributes' => array( 382 | 'type' => 'hidden', 383 | 'value' => $type, 384 | ), 385 | ); 386 | $group_args['fields'] = $subfields; 387 | $field->set_prop( 'fields', $subfields ); 388 | 389 | if ( isset( $layout['title'] ) ) { 390 | $group_args['options'] = array( 391 | 'group_title' => $layout['title'], 392 | ); 393 | $field = $field->get_field_clone( $group_args ); 394 | } 395 | 396 | $metabox->add_field( $group_args ); 397 | $field->index = $i; 398 | 399 | return $field; 400 | } 401 | 402 | /** 403 | * Value escape before output 404 | * 405 | * @param string $val Escaping default value. 406 | * @param array $meta_value Value of metadata. 407 | * @return array Escaped value of metadata 408 | */ 409 | public function escape_values( $val, $meta_value ) { 410 | if ( is_array( $meta_value ) && ! empty( $meta_value ) ) { 411 | foreach ( $meta_value as $i => $value ) { 412 | $meta_value[ $i ]['layout'] = esc_attr( $value['layout'] ); 413 | } 414 | } 415 | // Only need to escape the layout type. 416 | return $meta_value; 417 | } 418 | 419 | } 420 | 421 | RKV_CMB2_Flexible_Content_Field::get_instance(); 422 | } // End if(). 423 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reaktivstudios/cmb2-flexible-content", 3 | "description": "A flexible content field for CMB2.", 4 | "type": "wordpress-plugin", 5 | "license": "GPL-2.0", 6 | "homepage": "https://github.com/reaktivstudios/cmb2-flexible-content", 7 | "authors": [ 8 | { 9 | "name": "Reaktiv Studios", 10 | "email": "info@reaktivstudios.com", 11 | "homepage": "https://github.com/reaktivstudios" 12 | } 13 | ], 14 | "support": { 15 | "issues": "https://github.com/reaktivstudios/cmb2-flexible-content" 16 | }, 17 | "require": { 18 | "php": ">=5.3.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reaktivstudios/cmb2-flexible-content/d4c64a9468a0dabb4c5689f4141757ff0f83d702/screenshot-1.png --------------------------------------------------------------------------------