├── 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 |  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 '