├── README.md └── acf-repeater ├── acf-repeater-update.php ├── acf-repeater.php └── includes ├── 4-0 ├── acf-repeater-field.php ├── field-group.js ├── input.css └── input.js ├── 5-0 ├── acf-repeater-field.php ├── field-group.js ├── input.css └── input.js └── 5-7 ├── acf-repeater-field.php ├── field-group.js ├── input.css └── input.js /README.md: -------------------------------------------------------------------------------- 1 | # Advanced Custom Fields: Repeater Field 2 | 3 | # Installation 4 | 5 | This software can be treated as both a WP plugin and a theme include. 6 | However, only when activated as a plugin will updates be available 7 | 8 | # Plugin 9 | 1. Copy the 'acf-repeater' folder into your plugins folder 10 | 2. Activate the plugin via the Plugins admin page 11 | 12 | 13 | # Changelog 14 | 15 | = 2.2.0 = 16 | * Added support for ACF version 6.1.6 17 | 18 | = 2.1.0 = 19 | * Added support for ACF version 5.7.0 20 | 21 | = 2.0.1 = 22 | * Minor fixes and improvements 23 | 24 | = 2.0.0 = 25 | * Added support for ACF version 5 26 | 27 | = 1.1.1 = 28 | * Fixed CSS bug causing any nested flexible content fields to not display add/remove buttons for each layout 29 | * Fixes CSS bug causing cropped tables on small screen sizes 30 | 31 | = 1.1.0 = 32 | * Added Support for sub field conditional logic 33 | * Added Support for sub field required validation 34 | * Updated UI for 'column width' option (prepend %) 35 | * Updated UI for 'Maximum Rows' option (can be left blank) 36 | * Updated JS to use .on function instead of .live 37 | * Added new update script allowing distribution in premium plugins / themes 38 | 39 | = 1.0.1 = 40 | * [Updated] Updated sub field type list to remove 'Tab' as this does not work as a sub field 41 | 42 | = 1.0.0 = 43 | * [Updated] Updated update_field parameters 44 | * Official Release 45 | 46 | = 0.1.2 = 47 | * [IMPORTANT] This update requires the latest ACF v4 files available on GIT - https://github.com/elliotcondon/acf4 48 | * [Added] Added category to field to appear in the 'Layout' optgroup 49 | * [Updated] Updated dir / path code to use acf filter 50 | 51 | = 0.1.1 = 52 | * [IMPORTANT] This update requires the latest ACF v4 files available on GIT - https://github.com/elliotcondon/acf4 53 | * [Updated] Updated naming conventions in field group page 54 | 55 | = 0.1.0 = 56 | * [Fixed] Fix wrong str_replace in $dir 57 | 58 | = 0.0.9 = 59 | * [Updated] Drop support of old filters / actions 60 | 61 | = 0.0.8 = 62 | * [Fixed] Fix bug causing sub fields to not display choices correctly 63 | 64 | = 0.0.7 = 65 | * [IMPORTANT] This update requires the latest ACF v4 files available on GIT - https://github.com/elliotcondon/acf4 66 | * [Fixed] Fixed bug where field would appear empty after saving the page. This was caused by a cache conflict which has now been avoided by using the format_value filter to load sub field values instead of load_value 67 | 68 | = 0.0.6 = 69 | * [Updated] Update save method to use uniqid for field keys, not pretty field keys 70 | 71 | = 0.0.5 = 72 | * [Fixed] Fix wrong css / js urls on WINDOWS server. 73 | 74 | = 0.0.4 = 75 | * [Fixed] Fix bug preventing WYSIWYG sub fields to load. 76 | 77 | = 0.0.3 = 78 | * [Fixed] Fix load_field hook issues with nested fields. 79 | 80 | = 0.0.2 = 81 | * [Fixed] acf_load_field-${parent_hook}-${child_hook} now works! 82 | 83 | = 0.0.1 = 84 | * Initial Release. 85 | -------------------------------------------------------------------------------- /acf-repeater/acf-repeater-update.php: -------------------------------------------------------------------------------- 1 | force_check = !empty($_GET['force-check']); 33 | 34 | 35 | // append update information to transient 36 | add_filter('pre_set_site_transient_update_plugins', array($this, 'modify_plugins_transient'), 10, 1); 37 | 38 | 39 | // modify plugin data visible in the 'View details' popup 40 | add_filter('plugins_api', array($this, 'modify_plugin_details'), 10, 3); 41 | } 42 | 43 | 44 | /* 45 | * add_plugin 46 | * 47 | * This function will register a plugin 48 | * 49 | * @type function 50 | * @date 8/4/17 51 | * @since 5.5.10 52 | * 53 | * @param $plugin (array) 54 | * @return n/a 55 | */ 56 | 57 | function add_plugin( $plugin ) { 58 | 59 | // validate 60 | $plugin = wp_parse_args($plugin, array( 61 | 'id' => '', 62 | 'key' => '', 63 | 'slug' => '', 64 | 'basename' => '', 65 | 'version' => '', 66 | )); 67 | 68 | // Check if is_plugin_active() function exists. This is required on the front end of the 69 | // site, since it is in a file that is normally only loaded in the admin. 70 | if( !function_exists( 'is_plugin_active' ) ) { 71 | require_once ABSPATH . 'wp-admin/includes/plugin.php'; 72 | } 73 | 74 | // add if is active plugin (not included in theme) 75 | if( is_plugin_active($plugin['basename']) ) { 76 | $this->plugins[ $plugin['basename'] ] = $plugin; 77 | } 78 | } 79 | 80 | 81 | /* 82 | * request 83 | * 84 | * This function will make a request to the ACF update server 85 | * 86 | * @type function 87 | * @date 8/4/17 88 | * @since 5.5.10 89 | * 90 | * @param $query (string) 91 | * @param $body (array) 92 | * @return (mixed) 93 | */ 94 | 95 | function request( $query = 'index.php', $body = null ) { 96 | 97 | // vars 98 | $url = 'https://connect.advancedcustomfields.com/' . $query; 99 | 100 | // development mode 101 | if( $this->dev ) { 102 | $url = 'http://connect/' . $query; 103 | acf_log('acf connect: '. $url, $body); 104 | } 105 | 106 | // post 107 | $raw_response = wp_remote_post( $url, array( 108 | 'timeout' => 10, 109 | 'body' => $body 110 | )); 111 | 112 | // wp error 113 | if( is_wp_error($raw_response) ) { 114 | return $raw_response; 115 | 116 | // http error 117 | } elseif( wp_remote_retrieve_response_code($raw_response) != 200 ) { 118 | return new WP_Error( 'server_error', wp_remote_retrieve_response_message($raw_response) ); 119 | } 120 | 121 | // decode response 122 | $json = json_decode( wp_remote_retrieve_body($raw_response), true ); 123 | 124 | // allow non json value 125 | if( $json === null ) { 126 | return wp_remote_retrieve_body($raw_response); 127 | } 128 | 129 | // return 130 | return $json; 131 | } 132 | 133 | 134 | /* 135 | * get_plugin_info 136 | * 137 | * This function will get plugin info and save as transient 138 | * 139 | * @type function 140 | * @date 9/4/17 141 | * @since 5.5.10 142 | * 143 | * @param $id (string) 144 | * @return (mixed) 145 | */ 146 | 147 | function get_plugin_info( $id = '' ) { 148 | 149 | // var 150 | $transient_name = 'acf_plugin_info_' . $id; 151 | 152 | // ignore cache (only once) 153 | if( $this->force_check ) { 154 | $this->force_check = false; 155 | 156 | // check cache 157 | } else { 158 | $transient = get_transient($transient_name); 159 | if( $transient !== false ) return $transient; 160 | } 161 | 162 | // connect 163 | $response = $this->request('v2/plugins/get-info?p='.$id); 164 | 165 | // convert string (misc error) to WP_Error object 166 | if( is_string($response) ) { 167 | $response = new WP_Error( 'server_error', esc_html($response) ); 168 | } 169 | 170 | // allow json to include expiration but force minimum and max for safety 171 | $expiration = $this->get_expiration($response, DAY_IN_SECONDS, MONTH_IN_SECONDS); 172 | 173 | // update transient 174 | set_transient($transient_name, $response, $expiration ); 175 | 176 | // return 177 | return $response; 178 | } 179 | 180 | 181 | /** 182 | * get_plugin_updates 183 | * 184 | * description 185 | * 186 | * @date 8/7/18 187 | * @since 5.6.9 188 | * 189 | * @param type $var Description. Default. 190 | * @return type Description. 191 | */ 192 | 193 | function get_plugin_updates() { 194 | 195 | // var 196 | $transient_name = 'acf_plugin_updates'; 197 | 198 | // ignore cache (only once) 199 | if( $this->force_check ) { 200 | $this->force_check = false; 201 | 202 | // check cache 203 | } else { 204 | $transient = get_transient($transient_name); 205 | if( $transient !== false ) return $transient; 206 | } 207 | 208 | // vars 209 | $post = array( 210 | 'plugins' => wp_json_encode($this->plugins), 211 | 'wp' => wp_json_encode(array( 212 | 'wp_name' => get_bloginfo('name'), 213 | 'wp_url' => home_url(), 214 | 'wp_version' => get_bloginfo('version'), 215 | 'wp_language' => get_bloginfo('language'), 216 | 'wp_timezone' => get_option('timezone_string'), 217 | )), 218 | 'acf' => wp_json_encode(array( 219 | 'acf_version' => get_option('acf_version'), 220 | 'acf_pro' => (defined('ACF_PRO') && ACF_PRO), 221 | )), 222 | ); 223 | 224 | // request 225 | $response = $this->request('v2/plugins/update-check', $post); 226 | 227 | // allow json to include expiration but force minimum and max for safety 228 | $expiration = $this->get_expiration($response, DAY_IN_SECONDS, MONTH_IN_SECONDS); 229 | 230 | // update transient 231 | set_transient($transient_name, $response, $expiration ); 232 | 233 | // return 234 | return $response; 235 | } 236 | 237 | /** 238 | * get_expiration 239 | * 240 | * This function safely gets the expiration value from a response 241 | * 242 | * @date 8/7/18 243 | * @since 5.6.9 244 | * 245 | * @param mixed $response The response from the server. Default false. 246 | * @param int $min The minimum expiration limit. Default 0. 247 | * @param int $max The maximum expiration limit. Default 0. 248 | * @return int 249 | */ 250 | 251 | function get_expiration( $response = false, $min = 0, $max = 0 ) { 252 | 253 | // vars 254 | $expiration = 0; 255 | 256 | // check 257 | if( is_array($response) && isset($response['expiration']) ) { 258 | $expiration = (int) $response['expiration']; 259 | } 260 | 261 | // min 262 | if( $expiration < $min ) { 263 | return $min; 264 | } 265 | 266 | // max 267 | if( $expiration > $max ) { 268 | return $max; 269 | } 270 | 271 | // return 272 | return $expiration; 273 | } 274 | 275 | /* 276 | * refresh_plugins_transient 277 | * 278 | * This function will refresh plugin update info to the transient 279 | * 280 | * @type function 281 | * @date 11/4/17 282 | * @since 5.5.10 283 | * 284 | * @param n/a 285 | * @return n/a 286 | */ 287 | 288 | function refresh_plugins_transient() { 289 | 290 | // vars 291 | $transient = get_site_transient('update_plugins'); 292 | 293 | // bail early if no transient 294 | if( empty($transient) ) return; 295 | 296 | // update (will trigger modify function) 297 | $this->force_check = true; 298 | set_site_transient( 'update_plugins', $transient ); 299 | } 300 | 301 | 302 | /* 303 | * modify_plugins_transient 304 | * 305 | * This function will connect to the ACF website and find update information 306 | * 307 | * @type function 308 | * @date 16/01/2014 309 | * @since 5.0.0 310 | * 311 | * @param $transient (object) 312 | * @return $transient 313 | */ 314 | 315 | function modify_plugins_transient( $transient ) { 316 | 317 | // bail early if no response (error) 318 | if( !isset($transient->response) ) { 319 | return $transient; 320 | } 321 | 322 | // fetch updates (this filter is called multiple times during a single page load) 323 | $updates = $this->get_plugin_updates(); 324 | 325 | // append 326 | if( is_array($updates) ) { 327 | foreach( $updates['plugins'] as $basename => $update ) { 328 | $transient->response[ $basename ] = (object) $update; 329 | } 330 | } 331 | 332 | // return 333 | return $transient; 334 | } 335 | 336 | 337 | /* 338 | * modify_plugin_details 339 | * 340 | * This function will populate the plugin data visible in the 'View details' popup 341 | * 342 | * @type function 343 | * @date 17/01/2014 344 | * @since 5.0.0 345 | * 346 | * @param $result (bool|object) 347 | * @param $action (string) 348 | * @param $args (object) 349 | * @return $result 350 | */ 351 | 352 | function modify_plugin_details( $result, $action = null, $args = null ) { 353 | 354 | // vars 355 | $plugin = false; 356 | 357 | 358 | // only for 'plugin_information' action 359 | if( $action !== 'plugin_information' ) return $result; 360 | 361 | 362 | // find plugin via slug 363 | foreach( $this->plugins as $p ) { 364 | 365 | if( $args->slug == $p['slug'] ) $plugin = $p; 366 | 367 | } 368 | 369 | 370 | // bail early if plugin not found 371 | if( !$plugin ) return $result; 372 | 373 | 374 | // connect 375 | $response = $this->get_plugin_info($plugin['id']); 376 | 377 | 378 | // bail early if no response 379 | if( !is_array($response) ) return $result; 380 | 381 | 382 | // remove tags (different context) 383 | unset($response['tags']); 384 | 385 | 386 | // convert to object 387 | $response = (object) $response; 388 | 389 | 390 | // sections 391 | $sections = array( 392 | 'description' => '', 393 | 'installation' => '', 394 | 'changelog' => '', 395 | 'upgrade_notice' => '' 396 | ); 397 | 398 | foreach( $sections as $k => $v ) { 399 | 400 | $sections[ $k ] = $response->$k; 401 | 402 | } 403 | 404 | $response->sections = $sections; 405 | 406 | 407 | // return 408 | return $response; 409 | 410 | } 411 | 412 | } 413 | 414 | 415 | /* 416 | * acf_updates 417 | * 418 | * The main function responsible for returning the one true acf_updates instance to functions everywhere. 419 | * Use this function like you would a global variable, except without needing to declare the global. 420 | * 421 | * Example: 422 | * 423 | * @type function 424 | * @date 9/4/17 425 | * @since 5.5.12 426 | * 427 | * @param n/a 428 | * @return (object) 429 | */ 430 | 431 | function acf_updates() { 432 | 433 | global $acf_updates; 434 | 435 | if( !isset($acf_updates) ) { 436 | 437 | $acf_updates = new acf_updates(); 438 | 439 | } 440 | 441 | return $acf_updates; 442 | 443 | } 444 | 445 | 446 | /* 447 | * acf_register_plugin_update 448 | * 449 | * alias of acf_updates()->add_plugin() 450 | * 451 | * @type function 452 | * @date 12/4/17 453 | * @since 5.5.10 454 | * 455 | * @param $post_id (int) 456 | * @return $post_id (int) 457 | */ 458 | 459 | function acf_register_plugin_update( $plugin ) { 460 | 461 | acf_updates()->add_plugin( $plugin ); 462 | 463 | } 464 | 465 | 466 | endif; // class_exists check 467 | 468 | 469 | // register update 470 | acf_register_plugin_update(array( 471 | 'id' => 'repeater', 472 | 'key' => 'QJF7-L4IX-UCNP-RF2W', 473 | 'slug' => $this->settings['slug'], 474 | 'basename' => $this->settings['basename'], 475 | 'version' => $this->settings['version'], 476 | )); 477 | 478 | ?> -------------------------------------------------------------------------------- /acf-repeater/acf-repeater.php: -------------------------------------------------------------------------------- 1 | settings = array( 41 | 42 | // basic 43 | 'name' => __('Advanced Custom Fields: Repeater Field', 'acf'), 44 | 'version' => '2.2.0', 45 | 46 | // urls 47 | 'slug' => dirname(plugin_basename( __FILE__ )), 48 | 'basename' => plugin_basename( __FILE__ ), 49 | 'path' => plugin_dir_path( __FILE__ ), 50 | 'dir' => plugin_dir_url( __FILE__ ), 51 | 52 | ); 53 | 54 | // include v5 field 55 | add_action('acf/include_field_types', array($this, 'include_field_types')); 56 | 57 | // include v4 field 58 | add_action('acf/register_fields', array($this, 'include_field_types')); 59 | 60 | // include updates 61 | if( is_admin() ) { 62 | $this->include_file('acf-repeater-update.php'); 63 | } 64 | 65 | } 66 | 67 | 68 | /* 69 | * include_file 70 | * 71 | * This function will check if a file exists before including it 72 | * 73 | * @type function 74 | * @date 22/2/17 75 | * @since 5.5.8 76 | * 77 | * @param $file (string) 78 | * @return n/a 79 | */ 80 | 81 | function include_file( $file = '' ) { 82 | $file = dirname(__FILE__) . '/'. $file; 83 | if( file_exists($file) ) include_once( $file ); 84 | } 85 | 86 | 87 | /* 88 | * include_field_types 89 | * 90 | * This function will include the v5 field type 91 | * 92 | * @type function 93 | * @date 12/06/2015 94 | * @since 5.2.3 95 | * 96 | * @param n/a 97 | * @return n/a 98 | */ 99 | 100 | function include_field_types() { 101 | 102 | // vars 103 | $version = ''; 104 | 105 | // version 5 106 | if( defined('ACF_VERSION') ) { 107 | 108 | // version 5.7+ 109 | if( version_compare(ACF_VERSION, '5.7.0', '>=') ) { 110 | $version = '5-7'; 111 | 112 | // version 5.0 113 | } else { 114 | $version = '5-0'; 115 | } 116 | 117 | // version 4 118 | } else { 119 | $version = '4-0'; 120 | } 121 | 122 | // include 123 | $this->include_file( "includes/$version/acf-repeater-field.php" ); 124 | } 125 | } 126 | 127 | 128 | // globals 129 | global $acf_plugin_repeater; 130 | 131 | 132 | // instantiate 133 | $acf_plugin_repeater = new acf_plugin_repeater(); 134 | 135 | 136 | // end class 137 | endif; 138 | 139 | ?> -------------------------------------------------------------------------------- /acf-repeater/includes/4-0/acf-repeater-field.php: -------------------------------------------------------------------------------- 1 | name = 'repeater'; 22 | $this->label = __("Repeater",'acf'); 23 | $this->category = __("Layout",'acf'); 24 | $this->defaults = array( 25 | 'sub_fields' => array(), 26 | 'row_min' => 0, 27 | 'row_limit' => 0, 28 | 'layout' => 'table', 29 | 'button_label' => __("Add Row",'acf'), 30 | ); 31 | $this->l10n = array( 32 | 'min' => __("Minimum rows reached ( {min} rows )",'acf'), 33 | 'max' => __("Maximum rows reached ( {max} rows )",'acf'), 34 | ); 35 | 36 | 37 | // do not delete! 38 | parent::__construct(); 39 | 40 | 41 | // settings 42 | $this->settings = array( 43 | 'path' => apply_filters('acf/helpers/get_path', __FILE__), 44 | 'dir' => apply_filters('acf/helpers/get_dir', __FILE__), 45 | 'version' => '1.1.1' 46 | ); 47 | 48 | 49 | } 50 | 51 | 52 | /* 53 | * input_admin_enqueue_scripts() 54 | * 55 | * This action is called in the admin_enqueue_scripts action on the edit screen where your field is created. 56 | * Use this action to add css + javascript to assist your create_field() action. 57 | * 58 | * $info http://codex.wordpress.org/Plugin_API/Action_Reference/admin_enqueue_scripts 59 | * @type action 60 | * @since 3.6 61 | * @date 23/01/13 62 | */ 63 | 64 | function input_admin_enqueue_scripts() 65 | { 66 | // register acf scripts 67 | wp_register_script( 'acf-input-repeater', $this->settings['dir'] . 'input.js', array('acf-input') ); 68 | wp_register_style( 'acf-input-repeater', $this->settings['dir'] . 'input.css', array('acf-input') ); 69 | 70 | 71 | // scripts 72 | wp_enqueue_script(array( 73 | 'acf-input-repeater', 74 | )); 75 | 76 | // styles 77 | wp_enqueue_style(array( 78 | 'acf-input-repeater', 79 | )); 80 | 81 | } 82 | 83 | 84 | /* 85 | * field_group_admin_enqueue_scripts() 86 | * 87 | * This action is called in the admin_enqueue_scripts action on the edit screen where your field is edited. 88 | * Use this action to add css + javascript to assist your create_field_options() action. 89 | * 90 | * $info http://codex.wordpress.org/Plugin_API/Action_Reference/admin_enqueue_scripts 91 | * @type action 92 | * @since 3.6 93 | * @date 23/01/13 94 | */ 95 | 96 | function field_group_admin_enqueue_scripts() 97 | { 98 | wp_register_script( 'acf-field-group-repeater', $this->settings['dir'] . 'field-group.js', array('acf-field-group') ); 99 | 100 | // scripts 101 | wp_enqueue_script(array( 102 | 'acf-field-group-repeater', 103 | )); 104 | } 105 | 106 | 107 | /* 108 | * load_field() 109 | * 110 | * This filter is appied to the $field after it is loaded from the database 111 | * 112 | * @type filter 113 | * @since 3.6 114 | * @date 23/01/13 115 | * 116 | * @param $field - the field array holding all the field options 117 | * 118 | * @return $field - the field array holding all the field options 119 | */ 120 | 121 | function load_field( $field ) 122 | { 123 | // apply_load field to all sub fields 124 | if( isset($field['sub_fields']) && is_array($field['sub_fields']) ) 125 | { 126 | foreach( $field['sub_fields'] as $k => $sub_field ) 127 | { 128 | // apply filters 129 | $sub_field = apply_filters('acf/load_field_defaults', $sub_field); 130 | 131 | 132 | // apply filters 133 | foreach( array('type', 'name', 'key') as $key ) 134 | { 135 | // run filters 136 | $sub_field = apply_filters('acf/load_field/' . $key . '=' . $sub_field[ $key ], $sub_field); // new filter 137 | } 138 | 139 | 140 | // update sub field 141 | $field['sub_fields'][ $k ] = $sub_field; 142 | } 143 | } 144 | 145 | 146 | // return 147 | return $field; 148 | } 149 | 150 | 151 | /* 152 | * create_field() 153 | * 154 | * Create the HTML interface for your field 155 | * 156 | * @param $field - an array holding all the field's data 157 | * 158 | * @type action 159 | * @since 3.6 160 | * @date 23/01/13 161 | */ 162 | 163 | function create_field( $field ) 164 | { 165 | 166 | // validate 167 | $field['row_limit'] = intval( $field['row_limit'] ); 168 | $field['row_min'] = intval( $field['row_min'] ); 169 | 170 | 171 | // value may be false 172 | if( !is_array($field['value']) ) 173 | { 174 | $field['value'] = array(); 175 | } 176 | 177 | 178 | // row limit = 0? 179 | if( $field['row_limit'] < 1 ) 180 | { 181 | $field['row_limit'] = 999; 182 | } 183 | 184 | 185 | // min rows 186 | if( $field['row_min'] > count($field['value']) ) 187 | { 188 | for( $i = 0; $i < $field['row_min']; $i++ ) 189 | { 190 | // already have a value? continue... 191 | if( isset($field['value'][$i]) ) 192 | { 193 | continue; 194 | } 195 | 196 | 197 | // populate values 198 | $field['value'][ $i ] = array(); 199 | 200 | 201 | foreach( $field['sub_fields'] as $sub_field) 202 | { 203 | $sub_value = false; 204 | 205 | if( !empty($sub_field['default_value']) ) 206 | { 207 | $sub_value = $sub_field['default_value']; 208 | } 209 | 210 | $field['value'][ $i ][ $sub_field['key'] ] = $sub_value; 211 | } 212 | 213 | } 214 | } 215 | 216 | 217 | // max rows 218 | $row_count = count($field['value']); 219 | if( $row_count > $field['row_limit'] ) 220 | { 221 | for( $i = 0; $i < $row_count; $i++ ) 222 | { 223 | if( $i >= $field['row_limit'] ) 224 | { 225 | unset( $field['value'][ $i ] ); 226 | } 227 | } 228 | } 229 | 230 | 231 | // setup values for row clone 232 | $field['value']['acfcloneindex'] = array(); 233 | foreach( $field['sub_fields'] as $sub_field ) 234 | { 235 | $sub_value = false; 236 | 237 | if( isset($sub_field['default_value']) ) 238 | { 239 | $sub_value = $sub_field['default_value']; 240 | } 241 | 242 | 243 | $field['value']['acfcloneindex'][ $sub_field['key'] ] = $sub_value; 244 | } 245 | 246 | 247 | // helper function which does not exist yet in acf 248 | if( !function_exists('acf_get_join_attr') ): 249 | 250 | function acf_get_join_attr( $attributes = false ) 251 | { 252 | // validate 253 | if( empty($attributes) ) 254 | { 255 | return ''; 256 | } 257 | 258 | 259 | // vars 260 | $e = array(); 261 | 262 | 263 | // loop through and render 264 | foreach( $attributes as $k => $v ) 265 | { 266 | $e[] = $k . '="' . esc_attr( $v ) . '"'; 267 | } 268 | 269 | 270 | // echo 271 | return implode(' ', $e); 272 | } 273 | 274 | endif; 275 | 276 | if( !function_exists('acf_join_attr') ): 277 | 278 | function acf_join_attr( $attributes = false ) 279 | { 280 | echo acf_get_join_attr( $attributes ); 281 | } 282 | 283 | endif; 284 | 285 | ?> 286 |
287 | 288 | 289 | 290 | 291 | 1 ): ?> 296 | 297 | 298 | 299 | 1 && isset($sub_field['column_width']) && $sub_field['column_width'] ) 305 | { 306 | $attr = 'width="' . $sub_field['column_width'] . '%"'; 307 | } 308 | 309 | // required 310 | $required_label = ""; 311 | 312 | if( $sub_field['required'] ) 313 | { 314 | $required_label = ' *'; 315 | } 316 | 317 | ?> 318 | 325 | 326 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | $value ): ?> 338 | 339 | "> 340 | 341 | 1 ): ?> 346 | 347 | 348 | 349 | 354 | 442 | 443 | 444 | 449 | 453 | 454 | 455 | 456 | 457 | 458 |
> 319 | 320 | 321 | 322 | 323 |
355 | 356 | 357 | 358 | 359 | 364 | 365 | "field sub_field field_type-{$sub_field['type']} field_key-{$sub_field['key']}", 370 | 'data-field_type' => $sub_field['type'], 371 | 'data-field_key' => $sub_field['key'], 372 | 'data-field_name' => $sub_field['name'] 373 | ); 374 | 375 | 376 | // required 377 | if( $sub_field['required'] ) 378 | { 379 | $attributes['class'] .= ' required'; 380 | } 381 | 382 | 383 | // layout: Row 384 | 385 | if( $field['layout'] == 'row' ): ?> 386 | > 387 | 396 | 397 | 398 | 424 | 425 | 430 | 431 | 432 | 433 | 434 | 435 | 440 |
388 | 392 | 393 | 394 | 395 | > 399 |
400 | 422 |
423 |
441 |
450 | 451 | 452 |
459 | 460 | 461 | 466 | 467 | 468 |
469 | 'field_clone', 509 | 'label' => __("New Field",'acf'), 510 | 'name' => __("new_field",'acf'), 511 | 'type' => 'text', 512 | )); 513 | 514 | 515 | // get name of all fields for use in field type drop down 516 | $fields_names = apply_filters('acf/registered_fields', array()); 517 | unset( $fields_names[ __("Layout",'acf') ]['tab'] ); 518 | 519 | 520 | // conditional logic dummy data 521 | $conditional_logic_rule = array( 522 | 'field' => '', 523 | 'operator' => '==', 524 | 'value' => '' 525 | ); 526 | 527 | 528 | ?> 529 | 530 | 531 | 532 | 533 | 534 |
535 |
536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 |
546 |
547 |
548 | 549 |
1){ echo 'style="display:none;"'; } ?>> 550 | 551 |
552 | 553 | 558 |
559 | 560 |
561 | 562 | 563 | 564 | 575 | 576 | 577 | 578 |
565 | 566 | " href="javascript:;"> 567 | 568 | 574 |
579 |
580 | 581 |
582 |
583 | 584 | 585 | 586 | 587 | 591 | 601 | 602 | 603 | 607 | 617 | 618 | 619 | 620 | 632 | 633 | 634 | 635 | 651 | 652 | 653 | 654 | 668 | 669 | 670 | 673 | 690 | 691 | 697 | 698 | 699 | 784 | 785 | 786 | 789 | 796 | 797 | 798 |
588 | 589 |

590 |
592 | 'text', 595 | 'name' => 'fields[' . $fake_name . '][label]', 596 | 'value' => $sub_field['label'], 597 | 'class' => 'label', 598 | )); 599 | ?> 600 |
604 | 605 |

606 |
608 | 'text', 611 | 'name' => 'fields[' . $fake_name . '][name]', 612 | 'value' => $sub_field['name'], 613 | 'class' => 'name', 614 | )); 615 | ?> 616 |
621 | 'select', 624 | 'name' => 'fields[' . $fake_name . '][type]', 625 | 'value' => $sub_field['type'], 626 | 'class' => 'type', 627 | 'choices' => $fields_names, 628 | 'optgroup' => true 629 | )); 630 | ?> 631 |
636 | 'text', 645 | 'name' => 'fields[' . $fake_name . '][instructions]', 646 | 'value' => $sub_field['instructions'], 647 | 'class' => 'instructions', 648 | )); 649 | ?> 650 |
655 | 'radio', 658 | 'name' => 'fields[' .$fake_name . '][required]', 659 | 'value' => $sub_field['required'], 660 | 'choices' => array( 661 | 1 => __("Yes",'acf'), 662 | 0 => __("No",'acf'), 663 | ), 664 | 'layout' => 'horizontal', 665 | )); 666 | ?> 667 |
671 | 672 | 674 | 'number', 683 | 'name' => 'fields[' . $fake_name . '][column_width]', 684 | 'value' => $sub_field['column_width'], 685 | 'class' => 'column_width', 686 | 'append' => '%' 687 | )); 688 | ?> 689 |
700 | 'radio', 703 | 'name' => 'fields[' . $fake_name . '][conditional_logic][status]', 704 | 'value' => $sub_field['conditional_logic']['status'], 705 | 'choices' => array( 706 | 1 => __("Yes",'acf'), 707 | 0 => __("No",'acf'), 708 | ), 709 | 'layout' => 'horizontal', 710 | )); 711 | 712 | 713 | // no rules? 714 | if( ! $sub_field['conditional_logic']['rules'] ) 715 | { 716 | $sub_field['conditional_logic']['rules'] = array( 717 | array() // this will get merged with $conditional_logic_rule 718 | ); 719 | } 720 | 721 | ?> 722 |
> 723 | 724 | 725 | $rule ): 726 | 727 | // validate 728 | $rule = array_merge($conditional_logic_rule, $rule); 729 | 730 | 731 | // fix PHP error in 3.5.4.1 732 | if( strpos($rule['value'],'Undefined index: value in') !== false ) 733 | { 734 | $rule['value'] = ''; 735 | } 736 | 737 | ?> 738 | 739 | 742 | 755 | 756 | 762 | 763 | 764 | 765 |
740 | 741 | 743 | 'select', 746 | 'name' => 'fields[' . $fake_name . '][conditional_logic][rules][' . $rule_i . '][operator]', 747 | 'value' => $rule['operator'], 748 | 'choices' => array( 749 | '==' => __("is equal to",'acf'), 750 | '!=' => __("is not equal to",'acf'), 751 | ), 752 | )); 753 | ?> 754 | 757 |
    758 |
  • 759 |
  • 760 |
761 |
766 | 767 |
    768 |
  • 769 |
  • 'select', 771 | 'name' => 'fields[' . $fake_name . '][conditional_logic][allorany]', 772 | 'value' => $sub_field['conditional_logic']['allorany'], 773 | 'choices' => array( 774 | 'all' => __("all",'acf'), 775 | 'any' => __("any",'acf'), 776 | ), 777 | )); ?>
  • 778 |
  • 779 |
780 | 781 |
782 | 783 |
787 | 788 | 790 | 795 |
799 | 800 |
801 |
802 | 803 |
804 | 805 |
806 | 810 |
811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 'text', 821 | 'name' => 'fields['.$key.'][row_min]', 822 | 'value' => $field['row_min'], 823 | )); 824 | ?> 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 'text', 835 | 'name' => 'fields['.$key.'][row_limit]', 836 | 'value' => $field['row_limit'], 837 | )); 838 | ?> 839 | 840 | 841 | 842 | 843 | 844 | 845 | 846 | 'radio', 849 | 'name' => 'fields['.$key.'][layout]', 850 | 'value' => $field['layout'], 851 | 'layout' => 'horizontal', 852 | 'choices' => array( 853 | 'table' => __("Table",'acf'), 854 | 'row' => __("Row",'acf') 855 | ) 856 | )); 857 | ?> 858 | 859 | 860 | 861 | 862 | 863 | 864 | 865 | 'text', 868 | 'name' => 'fields['.$key.'][button_label]', 869 | 'value' => $field['button_label'], 870 | )); 871 | ?> 872 | 873 | 874 | $total ) 941 | { 942 | for ( $j = $total; $j < $old_total; $j++ ) 943 | { 944 | foreach( $field['sub_fields'] as $sub_field ) 945 | { 946 | do_action('acf/delete_value', $post_id, $field['name'] . '_' . $j . '_' . $sub_field['name'] ); 947 | } 948 | } 949 | } 950 | 951 | 952 | 953 | 954 | // update $value and return to allow for the normal save function to run 955 | $value = $total; 956 | 957 | 958 | return $value; 959 | } 960 | 961 | 962 | /* 963 | * update_field() 964 | * 965 | * This filter is appied to the $field before it is saved to the database 966 | * 967 | * @type filter 968 | * @since 3.6 969 | * @date 23/01/13 970 | * 971 | * @param $field - the field array holding all the field options 972 | * @param $post_id - the field group ID (post_type = acf) 973 | * 974 | * @return $field - the modified field 975 | */ 976 | 977 | function update_field( $field, $post_id ) 978 | { 979 | // format sub_fields 980 | if( $field['sub_fields'] ) 981 | { 982 | // remove dummy field 983 | unset( $field['sub_fields']['field_clone'] ); 984 | 985 | 986 | // loop through and save fields 987 | $i = -1; 988 | $sub_fields = array(); 989 | 990 | 991 | foreach( $field['sub_fields'] as $key => $f ) 992 | { 993 | $i++; 994 | 995 | 996 | // order 997 | $f['order_no'] = $i; 998 | $f['key'] = $key; 999 | 1000 | 1001 | // save 1002 | $f = apply_filters('acf/update_field/type=' . $f['type'], $f, $post_id ); // new filter 1003 | 1004 | 1005 | // add 1006 | $sub_fields[] = $f; 1007 | } 1008 | 1009 | 1010 | // update sub fields 1011 | $field['sub_fields'] = $sub_fields; 1012 | 1013 | } 1014 | 1015 | 1016 | // return updated repeater field 1017 | return $field; 1018 | } 1019 | 1020 | 1021 | /* 1022 | * format_value() 1023 | * 1024 | * This filter is appied to the $value after it is loaded from the db and before it is passed to the create_field action 1025 | * 1026 | * @type filter 1027 | * @since 3.6 1028 | * @date 23/01/13 1029 | * 1030 | * @param $value - the value which was loaded from the database 1031 | * @param $post_id - the $post_id from which the value was loaded 1032 | * @param $field - the field array holding all the field options 1033 | * 1034 | * @return $value - the modified value 1035 | */ 1036 | 1037 | function format_value( $value, $post_id, $field ) 1038 | { 1039 | // vars 1040 | $values = array(); 1041 | 1042 | 1043 | if( $value > 0 ) 1044 | { 1045 | // loop through rows 1046 | for($i = 0; $i < $value; $i++) 1047 | { 1048 | // loop through sub fields 1049 | foreach( $field['sub_fields'] as $sub_field ) 1050 | { 1051 | // update full name 1052 | $key = $sub_field['key']; 1053 | $sub_field['name'] = $field['name'] . '_' . $i . '_' . $sub_field['name']; 1054 | 1055 | $v = apply_filters('acf/load_value', false, $post_id, $sub_field); 1056 | $v = apply_filters('acf/format_value', $v, $post_id, $sub_field); 1057 | 1058 | $values[ $i ][ $key ] = $v; 1059 | 1060 | } 1061 | } 1062 | } 1063 | 1064 | 1065 | // return 1066 | return $values; 1067 | } 1068 | 1069 | 1070 | /* 1071 | * format_value_for_api() 1072 | * 1073 | * This filter is appied to the $value after it is loaded from the db and before it is passed back to the api functions such as the_field 1074 | * 1075 | * @type filter 1076 | * @since 3.6 1077 | * @date 23/01/13 1078 | * 1079 | * @param $value - the value which was loaded from the database 1080 | * @param $post_id - the $post_id from which the value was loaded 1081 | * @param $field - the field array holding all the field options 1082 | * 1083 | * @return $value - the modified value 1084 | */ 1085 | 1086 | function format_value_for_api( $value, $post_id, $field ) 1087 | { 1088 | // vars 1089 | $values = array(); 1090 | 1091 | 1092 | if( $value > 0 ) 1093 | { 1094 | // loop through rows 1095 | for($i = 0; $i < $value; $i++) 1096 | { 1097 | // loop through sub fields 1098 | foreach( $field['sub_fields'] as $sub_field ) 1099 | { 1100 | // update full name 1101 | $key = $sub_field['name']; 1102 | $sub_field['name'] = $field['name'] . '_' . $i . '_' . $sub_field['name']; 1103 | 1104 | $v = apply_filters('acf/load_value', false, $post_id, $sub_field); 1105 | $v = apply_filters('acf/format_value_for_api', $v, $post_id, $sub_field); 1106 | 1107 | $values[ $i ][ $key ] = $v; 1108 | 1109 | } 1110 | } 1111 | } 1112 | 1113 | 1114 | // return 1115 | return $values; 1116 | } 1117 | 1118 | } 1119 | 1120 | new acf_field_repeater(); 1121 | 1122 | ?> -------------------------------------------------------------------------------- /acf-repeater/includes/4-0/field-group.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | 3 | var repeater = { 4 | 5 | $el : null, 6 | 7 | set : function( o ){ 8 | 9 | // merge in new option 10 | $.extend( this, o ); 11 | 12 | 13 | // return this for chaining 14 | return this; 15 | 16 | }, 17 | 18 | init : function(){ 19 | 20 | this.render(); 21 | 22 | }, 23 | 24 | render : function(){ 25 | 26 | // vars 27 | var id = this.$el.attr('data-id'), 28 | layout = 'table'; 29 | 30 | 31 | // find layout value 32 | if( this.$el.find('input[name="fields[' + id + '][layout]"]:checked').length > 0 ) 33 | { 34 | layout = this.$el.find('input[name="fields[' + id + '][layout]"]:checked').val(); 35 | } 36 | 37 | 38 | // add class 39 | this.$el.find('.repeater:first').removeClass('layout-row layout-table').addClass( 'layout-' + layout ); 40 | 41 | } 42 | 43 | }; 44 | 45 | 46 | /* 47 | * Document Ready 48 | * 49 | * description 50 | * 51 | * @type function 52 | * @date 18/08/13 53 | * 54 | * @param $post_id (int) 55 | * @return $post_id (int) 56 | */ 57 | 58 | $(document).ready(function(){ 59 | 60 | $('.field_type-repeater').each(function(){ 61 | 62 | repeater.set({ $el : $(this) }).init(); 63 | 64 | }); 65 | 66 | }); 67 | 68 | 69 | /* 70 | * Events 71 | * 72 | * jQuery events for this field 73 | * 74 | * @type function 75 | * @date 1/03/2011 76 | * 77 | * @param N/A 78 | * @return N/A 79 | */ 80 | 81 | $(document).on('click', '.field_option_repeater_layout input[type="radio"]', function( e ){ 82 | 83 | repeater.set({ $el : $(this).closest('.field_type-repeater') }).render(); 84 | 85 | }); 86 | 87 | 88 | $(document).on('acf/field_form-open', function(e, field){ 89 | 90 | // vars 91 | $el = $(field); 92 | 93 | 94 | if( $el.hasClass('field_type-repeater') ) 95 | { 96 | repeater.set({ $el : $el }).render(); 97 | } 98 | 99 | }); 100 | 101 | 102 | })(jQuery); -------------------------------------------------------------------------------- /acf-repeater/includes/4-0/input.css: -------------------------------------------------------------------------------- 1 | .repeater { 2 | 3 | } 4 | 5 | .repeater > table { 6 | margin: 0 0 8px; 7 | } 8 | 9 | .repeater > table.acf-input-table > tbody > tr > td.field { 10 | padding: 8px; 11 | } 12 | 13 | .repeater > table > thead > tr > th:last-child { 14 | border-right: 0 none; 15 | } 16 | 17 | .repeater > table > tbody > tr > td.remove > a.acf-button-add, 18 | .repeater > table > tbody > tr > td.remove > a.acf-button-remove { 19 | position: absolute; 20 | 21 | -webkit-transition: opacity 0.25s 0s ease-in-out, visibility 0s linear 0.25s; 22 | -moz-transition: opacity 0.25s 0s ease-in-out, visibility 0s linear 0.25s; 23 | -o-transition: opacity 0.25s 0s ease-in-out, visibility 0s linear 0.25s; 24 | transition: opacity 0.25s 0s ease-in-out, visibility 0s linear 0.25s; 25 | 26 | visibility: hidden; 27 | opacity: 0; 28 | } 29 | 30 | .repeater > table > tbody > tr > td.remove > a.acf-button-remove { 31 | position: relative; 32 | } 33 | 34 | .repeater > table > tbody > tr:hover > td.remove > a.acf-button-add, 35 | .repeater > table > tbody > tr:hover > td.remove > a.acf-button-remove { 36 | 37 | -webkit-transition-delay:0s; 38 | -moz-transition-delay:0s; 39 | -o-transition-delay:0s; 40 | transition-delay:0s; 41 | 42 | visibility: visible; 43 | opacity: 1; 44 | } 45 | 46 | .repeater.disabled > table > tbody > tr:hover > td.remove > a.acf-button-add { 47 | visibility: hidden !important; 48 | } 49 | 50 | .repeater > table > thead > tr > th.order, 51 | .repeater > table > tbody > tr > td.order{ 52 | width: 16px !important; 53 | text-align: center !important; 54 | vertical-align: middle; 55 | color: #aaa; 56 | text-shadow: #fff 0 1px 0; 57 | 58 | cursor: move; 59 | } 60 | 61 | .repeater > table > tbody > tr > td.order { 62 | border-right-color: #E1E1E1; 63 | background: #f4f4f4; 64 | } 65 | 66 | .repeater > table > tbody > tr > td.remove { 67 | background: #F9F9F9; 68 | } 69 | 70 | 71 | .repeater > table > thead > tr > th.order:hover, 72 | .repeater > table > tbody > tr > td.order:hover { 73 | color: #666; 74 | } 75 | 76 | .repeater > table > thead > tr > th.remove, 77 | .repeater > table > tbody > tr > td.remove{ 78 | width: 16px !important; 79 | vertical-align: middle; 80 | } 81 | 82 | .repeater tr.row-clone { 83 | display: none !important; 84 | } 85 | 86 | .repeater > table > tbody > tr.ui-sortable-helper { 87 | box-shadow: 0 1px 5px rgba(0,0,0,0.2); 88 | } 89 | 90 | .repeater > table > tbody > tr.ui-sortable-placeholder { 91 | visibility: visible !important; 92 | } 93 | 94 | .repeater > table > tbody > tr.ui-sortable-placeholder td { 95 | border: 0 none !important; 96 | box-shadow: inset 0 1px 3px rgba(0,0,0,0.1); 97 | background: rgba(0,0,0,0.075); 98 | } 99 | 100 | 101 | /* 102 | * Empty 103 | */ 104 | 105 | .repeater.empty table thead th { 106 | border-bottom: 0 none; 107 | } 108 | 109 | .repeater.empty table.row_layout { 110 | box-shadow: none; 111 | border: 0 none; 112 | margin: 0; 113 | } 114 | 115 | 116 | /* 117 | * Conditiona Logic 118 | */ 119 | 120 | 121 | 122 | 123 | /*--------------------------------------------------------------------------------------------- 124 | * 125 | * Everythign Field fixes 126 | * 127 | *---------------------------------------------------------------------------------------------*/ 128 | 129 | .media-item .describe .repeater > table > thead > tr > th { 130 | width: auto; 131 | } 132 | -------------------------------------------------------------------------------- /acf-repeater/includes/4-0/input.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | 3 | /* 4 | * Repeater 5 | * 6 | * static model for this field 7 | * 8 | * @type event 9 | * @date 18/08/13 10 | * 11 | */ 12 | 13 | acf.fields.repeater = { 14 | 15 | $el : null, 16 | 17 | o : {}, 18 | 19 | set : function( o ){ 20 | 21 | // merge in new option 22 | $.extend( this, o ); 23 | 24 | 25 | // find elements 26 | //this.$input = this.$el.children('input[type="hidden"]'); 27 | 28 | 29 | // get options 30 | this.o = acf.helpers.get_atts( this.$el ); 31 | 32 | 33 | // add row_count 34 | this.o.row_count = this.$el.find('> table > tbody > tr.row').length; 35 | 36 | 37 | // return this for chaining 38 | return this; 39 | 40 | }, 41 | init : function(){ 42 | 43 | // reference 44 | var _this = this, 45 | $el = this.$el; 46 | 47 | 48 | // sortable 49 | if( this.o.max_rows != 1 ) 50 | { 51 | this.$el.find('> table > tbody').unbind('sortable').sortable({ 52 | 53 | items : '> tr.row', 54 | handle : '> td.order', 55 | helper : acf.helpers.sortable, 56 | forceHelperSize : true, 57 | forcePlaceholderSize : true, 58 | scroll : true, 59 | 60 | start : function (event, ui) { 61 | 62 | $(document).trigger('acf/sortable_start', ui.item); 63 | $(document).trigger('acf/sortable_start_repeater', ui.item); 64 | 65 | // add markup to the placeholder 66 | var td_count = ui.item.children('td').length; 67 | ui.placeholder.html(''); 68 | 69 | }, 70 | 71 | stop : function (event, ui) { 72 | 73 | $(document).trigger('acf/sortable_stop', ui.item); 74 | $(document).trigger('acf/sortable_stop_repeater', ui.item); 75 | 76 | 77 | // render 78 | _this.set({ $el : $el }).render(); 79 | 80 | } 81 | }); 82 | } 83 | 84 | 85 | // render 86 | this.render(); 87 | 88 | }, 89 | render : function(){ 90 | 91 | // update row_count 92 | this.o.row_count = this.$el.find('> table > tbody > tr.row').length; 93 | 94 | 95 | // update order numbers 96 | this.$el.find('> table > tbody > tr.row').each(function(i){ 97 | 98 | $(this).children('td.order').html( i+1 ); 99 | 100 | }); 101 | 102 | 103 | // empty? 104 | if( this.o.row_count == 0 ) 105 | { 106 | this.$el.addClass('empty'); 107 | } 108 | else 109 | { 110 | this.$el.removeClass('empty'); 111 | } 112 | 113 | 114 | // row limit reached 115 | if( this.o.row_count >= this.o.max_rows ) 116 | { 117 | this.$el.addClass('disabled'); 118 | this.$el.find('> .repeater-footer .acf-button').addClass('disabled'); 119 | } 120 | else 121 | { 122 | this.$el.removeClass('disabled'); 123 | this.$el.find('> .repeater-footer .acf-button').removeClass('disabled'); 124 | } 125 | 126 | }, 127 | add : function( $before ){ 128 | 129 | 130 | // validate 131 | if( this.o.row_count >= this.o.max_rows ) 132 | { 133 | alert( acf.l10n.repeater.max.replace('{max}', this.o.max_rows) ); 134 | return false; 135 | } 136 | 137 | 138 | // create and add the new field 139 | var new_id = acf.helpers.uniqid(), 140 | new_field_html = this.$el.find('> table > tbody > tr.row-clone').html().replace(/(=["]*[\w-\[\]]*?)(acfcloneindex)/g, '$1' + new_id), 141 | new_field = $('').append( new_field_html ); 142 | 143 | 144 | // add row 145 | if( ! $before ) 146 | { 147 | $before = this.$el.find('> table > tbody > .row-clone'); 148 | } 149 | 150 | $before.before( new_field ); 151 | 152 | 153 | // trigger mouseenter on parent repeater to work out css margin on add-row button 154 | this.$el.closest('tr').trigger('mouseenter'); 155 | 156 | 157 | // update order 158 | this.render(); 159 | 160 | 161 | // setup fields 162 | $(document).trigger('acf/setup_fields', new_field); 163 | 164 | 165 | // validation 166 | this.$el.closest('.field').removeClass('error'); 167 | 168 | }, 169 | remove : function( $tr ){ 170 | 171 | // refernce 172 | var _this = this; 173 | 174 | 175 | // validate 176 | if( this.o.row_count <= this.o.min_rows ) 177 | { 178 | alert( acf.l10n.repeater.min.replace('{min}', this.o.min_rows) ); 179 | return false; 180 | } 181 | 182 | 183 | // animate out tr 184 | $tr.addClass('acf-remove-item'); 185 | setTimeout(function(){ 186 | 187 | $tr.remove(); 188 | 189 | 190 | // trigger mouseenter on parent repeater to work out css margin on add-row button 191 | _this.$el.closest('tr').trigger('mouseenter'); 192 | 193 | 194 | // render 195 | _this.render(); 196 | 197 | }, 400); 198 | 199 | } 200 | 201 | 202 | }; 203 | 204 | 205 | /* 206 | * acf/setup_fields 207 | * 208 | * run init function on all elements for this field 209 | * 210 | * @type event 211 | * @date 20/07/13 212 | * 213 | * @param {object} e event object 214 | * @param {object} el DOM object which may contain new ACF elements 215 | * @return N/A 216 | */ 217 | 218 | $(document).on('acf/setup_fields', function(e, el){ 219 | 220 | $(el).find('.repeater').each(function(){ 221 | 222 | acf.fields.repeater.set({ $el : $(this) }).init(); 223 | 224 | }); 225 | 226 | }); 227 | 228 | 229 | /* 230 | * Events 231 | * 232 | * jQuery events for this field 233 | * 234 | * @type function 235 | * @date 1/03/2011 236 | * 237 | * @param N/A 238 | * @return N/A 239 | */ 240 | 241 | $(document).on('click', '.repeater .repeater-footer .add-row-end', function( e ){ 242 | 243 | e.preventDefault(); 244 | 245 | acf.fields.repeater.set({ $el : $(this).closest('.repeater') }).add( false ); 246 | 247 | $(this).blur(); 248 | 249 | }); 250 | 251 | $(document).on('click', '.repeater td.remove .add-row-before', function( e ){ 252 | 253 | e.preventDefault(); 254 | 255 | acf.fields.repeater.set({ $el : $(this).closest('.repeater') }).add( $(this).closest('tr') ); 256 | 257 | $(this).blur(); 258 | 259 | }); 260 | 261 | $(document).on('click', '.repeater td.remove .acf-button-remove', function( e ){ 262 | 263 | e.preventDefault(); 264 | 265 | acf.fields.repeater.set({ $el : $(this).closest('.repeater') }).remove( $(this).closest('tr') ); 266 | 267 | $(this).blur(); 268 | 269 | }); 270 | 271 | $(document).on('mouseenter', '.repeater tr.row', function( e ){ 272 | 273 | // vars 274 | var $el = $(this).find('> td.remove > a.acf-button-add'), 275 | margin = ( $el.parent().height() / 2 ) + 9; // 9 = padding + border 276 | 277 | 278 | // css 279 | $el.css('margin-top', '-' + margin + 'px' ); 280 | 281 | }); 282 | 283 | $(document).on('acf/conditional_logic/show acf/conditional_logic/hide', function( e, $target, item ){ 284 | 285 | $target.closest('tr.row').trigger('mouseenter'); 286 | 287 | }); 288 | 289 | 290 | })(jQuery); -------------------------------------------------------------------------------- /acf-repeater/includes/5-0/acf-repeater-field.php: -------------------------------------------------------------------------------- 1 | name = 'repeater'; 25 | $this->label = __("Repeater",'acf'); 26 | $this->category = 'layout'; 27 | $this->defaults = array( 28 | 'sub_fields' => array(), 29 | 'min' => 0, 30 | 'max' => 0, 31 | 'layout' => 'table', 32 | 'button_label' => '', 33 | 'collapsed' => '' 34 | ); 35 | $this->l10n = array( 36 | 'min' => __("Minimum rows reached ({min} rows)",'acf'), 37 | 'max' => __("Maximum rows reached ({max} rows)",'acf'), 38 | ); 39 | 40 | 41 | // field filters 42 | $this->add_field_filter('acf/prepare_field_for_export', array($this, 'prepare_field_for_export')); 43 | $this->add_field_filter('acf/prepare_field_for_import', array($this, 'prepare_field_for_import')); 44 | 45 | 46 | // filters 47 | $this->add_filter('acf/validate_field', array($this, 'validate_any_field')); 48 | 49 | } 50 | 51 | 52 | /* 53 | * input_admin_enqueue_scripts 54 | * 55 | * description 56 | * 57 | * @type function 58 | * @date 16/12/2015 59 | * @since 5.3.2 60 | * 61 | * @param $post_id (int) 62 | * @return $post_id (int) 63 | */ 64 | 65 | function input_admin_enqueue_scripts() { 66 | 67 | wp_enqueue_script( 'acf-input-repeater', acf_get_external_dir(__FILE__, 'input.js'), array('acf-input') ); 68 | wp_enqueue_style( 'acf-input-repeater', acf_get_external_dir(__FILE__, 'input.css'), array('acf-input') ); 69 | 70 | } 71 | 72 | 73 | /* 74 | * field_group_admin_enqueue_scripts 75 | * 76 | * description 77 | * 78 | * @type function 79 | * @date 16/12/2015 80 | * @since 5.3.2 81 | * 82 | * @param $post_id (int) 83 | * @return $post_id (int) 84 | */ 85 | 86 | function field_group_admin_enqueue_scripts() { 87 | 88 | wp_enqueue_script( 'acf-field-group-repeater', acf_get_external_dir(__FILE__, 'field-group.js'), array('acf-field-group') ); 89 | 90 | } 91 | 92 | 93 | /* 94 | * load_field() 95 | * 96 | * This filter is appied to the $field after it is loaded from the database 97 | * 98 | * @type filter 99 | * @since 3.6 100 | * @date 23/01/13 101 | * 102 | * @param $field - the field array holding all the field options 103 | * 104 | * @return $field - the field array holding all the field options 105 | */ 106 | 107 | function load_field( $field ) { 108 | 109 | // min/max 110 | $field['min'] = (int) $field['min']; 111 | $field['max'] = (int) $field['max']; 112 | 113 | 114 | // vars 115 | $sub_fields = acf_get_fields( $field ); 116 | 117 | 118 | // append 119 | if( $sub_fields ) { 120 | 121 | $field['sub_fields'] = $sub_fields; 122 | 123 | } 124 | 125 | 126 | // return 127 | return $field; 128 | 129 | } 130 | 131 | 132 | /* 133 | * render_field() 134 | * 135 | * Create the HTML interface for your field 136 | * 137 | * @param $field - an array holding all the field's data 138 | * 139 | * @type action 140 | * @since 3.6 141 | * @date 23/01/13 142 | */ 143 | 144 | function render_field( $field ) { 145 | 146 | // vars 147 | $sub_fields = $field['sub_fields']; 148 | $show_order = true; 149 | $show_add = true; 150 | $show_remove = true; 151 | 152 | 153 | // bail early if no sub fields 154 | if( empty($sub_fields) ) return; 155 | 156 | 157 | // value 158 | $value = is_array($field['value']) ? $field['value'] : array(); 159 | 160 | 161 | // div 162 | $div = array( 163 | 'class' => 'acf-repeater', 164 | 'data-min' => $field['min'], 165 | 'data-max' => $field['max'] 166 | ); 167 | 168 | 169 | // empty 170 | if( empty($value) ) { 171 | 172 | $div['class'] .= ' -empty'; 173 | 174 | } 175 | 176 | 177 | // If there are less values than min, populate the extra values 178 | if( $field['min'] ) { 179 | 180 | $value = array_pad($value, $field['min'], array()); 181 | 182 | } 183 | 184 | 185 | // If there are more values than man, remove some values 186 | if( $field['max'] ) { 187 | 188 | $value = array_slice($value, 0, $field['max']); 189 | 190 | 191 | // if max 1 row, don't show order 192 | if( $field['max'] == 1 ) { 193 | 194 | $show_order = false; 195 | 196 | } 197 | 198 | 199 | // if max == min, don't show add or remove buttons 200 | if( $field['max'] <= $field['min'] ) { 201 | 202 | $show_remove = false; 203 | $show_add = false; 204 | 205 | } 206 | 207 | } 208 | 209 | 210 | // setup values for row clone 211 | $value['acfcloneindex'] = array(); 212 | 213 | 214 | // button label 215 | if( $field['button_label'] === '' ) $field['button_label'] = __('Add Row', 'acf'); 216 | 217 | 218 | // field wrap 219 | $el = 'td'; 220 | $before_fields = ''; 221 | $after_fields = ''; 222 | 223 | if( $field['layout'] == 'row' ) { 224 | 225 | $el = 'div'; 226 | $before_fields = ''; 227 | $after_fields = ''; 228 | 229 | } elseif( $field['layout'] == 'block' ) { 230 | 231 | $el = 'div'; 232 | 233 | $before_fields = ''; 234 | $after_fields = ''; 235 | 236 | } 237 | 238 | 239 | // layout 240 | $div['class'] .= ' -' . $field['layout']; 241 | 242 | 243 | // hidden input 244 | acf_hidden_input(array( 245 | 'type' => 'hidden', 246 | 'name' => $field['name'], 247 | )); 248 | 249 | 250 | // collapsed 251 | if( $field['collapsed'] ) { 252 | 253 | // add target class 254 | foreach( $sub_fields as $i => $sub_field ) { 255 | 256 | // bail early if no match 257 | if( $sub_field['key'] !== $field['collapsed'] ) continue; 258 | 259 | 260 | // class 261 | $sub_field['wrapper']['class'] .= ' -collapsed-target'; 262 | 263 | 264 | // update 265 | $sub_fields[ $i ] = $sub_field; 266 | 267 | } 268 | 269 | } 270 | 271 | ?> 272 |
> 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 309 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | $row ): 326 | 327 | $row_class = 'acf-row'; 328 | 329 | if( $i === 'acfcloneindex' ) { 330 | 331 | $row_class .= ' acf-clone'; 332 | 333 | } elseif( $field['collapsed'] && acf_is_row_collapsed($field['key'], $i) ) { 334 | 335 | $row_class .= ' -collapsed'; 336 | 337 | } 338 | 339 | ?> 340 | 341 | 342 | 343 | 349 | 350 | 351 | 352 | 353 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 393 | 394 | 395 | 396 | 397 | 398 |
> 310 | 311 | 312 |

313 | 314 |
344 | 345 | 346 | 347 | 348 | 390 | 391 | 392 |
399 | 400 | 401 | 406 | 407 | 408 |
409 | $field['sub_fields'], 432 | 'parent' => $field['ID'] 433 | ); 434 | 435 | 436 | ?> 437 | 438 | 439 |

440 | 441 | 442 | 447 | 448 | 449 | __('Collapsed','acf'), 469 | 'instructions' => __('Select a sub field to show when row is collapsed','acf'), 470 | 'type' => 'select', 471 | 'name' => 'collapsed', 472 | 'allow_null' => 1, 473 | 'choices' => $choices 474 | )); 475 | 476 | 477 | // min 478 | acf_render_field_setting( $field, array( 479 | 'label' => __('Minimum Rows','acf'), 480 | 'instructions' => '', 481 | 'type' => 'number', 482 | 'name' => 'min', 483 | 'placeholder' => '0', 484 | )); 485 | 486 | 487 | // max 488 | acf_render_field_setting( $field, array( 489 | 'label' => __('Maximum Rows','acf'), 490 | 'instructions' => '', 491 | 'type' => 'number', 492 | 'name' => 'max', 493 | 'placeholder' => '0', 494 | )); 495 | 496 | 497 | // layout 498 | acf_render_field_setting( $field, array( 499 | 'label' => __('Layout','acf'), 500 | 'instructions' => '', 501 | 'class' => 'acf-repeater-layout', 502 | 'type' => 'radio', 503 | 'name' => 'layout', 504 | 'layout' => 'horizontal', 505 | 'choices' => array( 506 | 'table' => __('Table','acf'), 507 | 'block' => __('Block','acf'), 508 | 'row' => __('Row','acf') 509 | ) 510 | )); 511 | 512 | 513 | // button_label 514 | acf_render_field_setting( $field, array( 515 | 'label' => __('Button Label','acf'), 516 | 'instructions' => '', 517 | 'type' => 'text', 518 | 'name' => 'button_label', 519 | 'placeholder' => __('Add Row','acf') 520 | )); 521 | 522 | } 523 | 524 | 525 | /* 526 | * load_value() 527 | * 528 | * This filter is applied to the $value after it is loaded from the db 529 | * 530 | * @type filter 531 | * @since 3.6 532 | * @date 23/01/13 533 | * 534 | * @param $value (mixed) the value found in the database 535 | * @param $post_id (mixed) the $post_id from which the value was loaded 536 | * @param $field (array) the field array holding all the field options 537 | * @return $value 538 | */ 539 | 540 | function load_value( $value, $post_id, $field ) { 541 | 542 | // bail early if no value 543 | if( empty($value) ) return false; 544 | 545 | 546 | // bail ealry if not numeric 547 | if( !is_numeric($value) ) return false; 548 | 549 | 550 | // bail early if no sub fields 551 | if( empty($field['sub_fields']) ) return false; 552 | 553 | 554 | // vars 555 | $value = intval($value); 556 | $rows = array(); 557 | 558 | 559 | // loop 560 | for( $i = 0; $i < $value; $i++ ) { 561 | 562 | // create empty array 563 | $rows[ $i ] = array(); 564 | 565 | 566 | // loop through sub fields 567 | foreach( array_keys($field['sub_fields']) as $j ) { 568 | 569 | // get sub field 570 | $sub_field = $field['sub_fields'][ $j ]; 571 | 572 | 573 | // bail ealry if no name (tab) 574 | if( acf_is_empty($sub_field['name']) ) continue; 575 | 576 | 577 | // update $sub_field name 578 | $sub_field['name'] = "{$field['name']}_{$i}_{$sub_field['name']}"; 579 | 580 | 581 | // get value 582 | $sub_value = acf_get_value( $post_id, $sub_field ); 583 | 584 | 585 | // add value 586 | $rows[ $i ][ $sub_field['key'] ] = $sub_value; 587 | 588 | } 589 | 590 | } 591 | 592 | 593 | // return 594 | return $rows; 595 | 596 | } 597 | 598 | 599 | /* 600 | * format_value() 601 | * 602 | * This filter is appied to the $value after it is loaded from the db and before it is returned to the template 603 | * 604 | * @type filter 605 | * @since 3.6 606 | * @date 23/01/13 607 | * 608 | * @param $value (mixed) the value which was loaded from the database 609 | * @param $post_id (mixed) the $post_id from which the value was loaded 610 | * @param $field (array) the field array holding all the field options 611 | * 612 | * @return $value (mixed) the modified value 613 | */ 614 | 615 | function format_value( $value, $post_id, $field ) { 616 | 617 | // bail early if no value 618 | if( empty($value) ) return false; 619 | 620 | 621 | // bail ealry if not array 622 | if( !is_array($value) ) return false; 623 | 624 | 625 | // bail early if no sub fields 626 | if( empty($field['sub_fields']) ) return false; 627 | 628 | 629 | // loop over rows 630 | foreach( array_keys($value) as $i ) { 631 | 632 | // loop through sub fields 633 | foreach( array_keys($field['sub_fields']) as $j ) { 634 | 635 | // get sub field 636 | $sub_field = $field['sub_fields'][ $j ]; 637 | 638 | 639 | // bail ealry if no name (tab) 640 | if( acf_is_empty($sub_field['name']) ) continue; 641 | 642 | 643 | // extract value 644 | $sub_value = acf_extract_var( $value[ $i ], $sub_field['key'] ); 645 | 646 | 647 | // update $sub_field name 648 | $sub_field['name'] = "{$field['name']}_{$i}_{$sub_field['name']}"; 649 | 650 | 651 | // format value 652 | $sub_value = acf_format_value( $sub_value, $post_id, $sub_field ); 653 | 654 | 655 | // append to $row 656 | $value[ $i ][ $sub_field['_name'] ] = $sub_value; 657 | 658 | } 659 | 660 | } 661 | 662 | 663 | // return 664 | return $value; 665 | 666 | } 667 | 668 | 669 | /* 670 | * validate_value 671 | * 672 | * description 673 | * 674 | * @type function 675 | * @date 11/02/2014 676 | * @since 5.0.0 677 | * 678 | * @param $post_id (int) 679 | * @return $post_id (int) 680 | */ 681 | 682 | function validate_value( $valid, $value, $field, $input ){ 683 | 684 | // remove acfcloneindex 685 | if( isset($value['acfcloneindex']) ) { 686 | 687 | unset($value['acfcloneindex']); 688 | 689 | } 690 | 691 | 692 | // valid 693 | if( $field['required'] && empty($value) ) { 694 | 695 | $valid = false; 696 | 697 | } 698 | 699 | 700 | // check sub fields 701 | if( !empty($field['sub_fields']) && !empty($value) ) { 702 | 703 | $keys = array_keys($value); 704 | 705 | foreach( $keys as $i ) { 706 | 707 | foreach( $field['sub_fields'] as $sub_field ) { 708 | 709 | // vars 710 | $k = $sub_field['key']; 711 | 712 | 713 | // test sub field exists 714 | if( !isset($value[ $i ][ $k ]) ) { 715 | 716 | continue; 717 | 718 | } 719 | 720 | 721 | // validate 722 | acf_validate_value( $value[ $i ][ $k ], $sub_field, "{$input}[{$i}][{$k}]" ); 723 | } 724 | 725 | } 726 | 727 | } 728 | 729 | return $valid; 730 | 731 | } 732 | 733 | 734 | /* 735 | * update_row 736 | * 737 | * This function will update a value row 738 | * 739 | * @type function 740 | * @date 15/2/17 741 | * @since 5.5.8 742 | * 743 | * @param $i (int) 744 | * @param $field (array) 745 | * @param $post_id (mixed) 746 | * @return (boolean) 747 | */ 748 | 749 | function update_row( $row, $i = 0, $field, $post_id ) { 750 | 751 | // bail early if no layout reference 752 | if( !is_array($row) ) return false; 753 | 754 | 755 | // bail early if no layout 756 | if( empty($field['sub_fields']) ) return false; 757 | 758 | 759 | // loop 760 | foreach( $field['sub_fields'] as $sub_field ) { 761 | 762 | // value 763 | $value = null; 764 | 765 | 766 | // find value (key) 767 | if( isset($row[ $sub_field['key'] ]) ) { 768 | 769 | $value = $row[ $sub_field['key'] ]; 770 | 771 | // find value (name) 772 | } elseif( isset($row[ $sub_field['name'] ]) ) { 773 | 774 | $value = $row[ $sub_field['name'] ]; 775 | 776 | // value does not exist 777 | } else { 778 | 779 | continue; 780 | 781 | } 782 | 783 | 784 | // modify name for save 785 | $sub_field['name'] = "{$field['name']}_{$i}_{$sub_field['name']}"; 786 | 787 | 788 | // update field 789 | acf_update_value( $value, $post_id, $sub_field ); 790 | 791 | } 792 | 793 | 794 | // return 795 | return true; 796 | 797 | } 798 | 799 | 800 | /* 801 | * delete_row 802 | * 803 | * This function will delete a value row 804 | * 805 | * @type function 806 | * @date 15/2/17 807 | * @since 5.5.8 808 | * 809 | * @param $i (int) 810 | * @param $field (array) 811 | * @param $post_id (mixed) 812 | * @return (boolean) 813 | */ 814 | 815 | function delete_row( $i = 0, $field, $post_id ) { 816 | 817 | // bail early if no sub fields 818 | if( empty($field['sub_fields']) ) return false; 819 | 820 | 821 | // loop 822 | foreach( $field['sub_fields'] as $sub_field ) { 823 | 824 | // modify name for delete 825 | $sub_field['name'] = "{$field['name']}_{$i}_{$sub_field['name']}"; 826 | 827 | 828 | // delete value 829 | acf_delete_value( $post_id, $sub_field ); 830 | 831 | } 832 | 833 | 834 | // return 835 | return true; 836 | 837 | } 838 | 839 | 840 | /* 841 | * update_value() 842 | * 843 | * This filter is appied to the $value before it is updated in the db 844 | * 845 | * @type filter 846 | * @since 3.6 847 | * @date 23/01/13 848 | * 849 | * @param $value - the value which will be saved in the database 850 | * @param $field - the field array holding all the field options 851 | * @param $post_id - the $post_id of which the value will be saved 852 | * 853 | * @return $value - the modified value 854 | */ 855 | 856 | function update_value( $value, $post_id, $field ) { 857 | 858 | // bail early if no sub fields 859 | if( empty($field['sub_fields']) ) return $value; 860 | 861 | 862 | // vars 863 | $new_value = 0; 864 | $old_value = (int) acf_get_metadata( $post_id, $field['name'] ); 865 | 866 | 867 | // update sub fields 868 | if( !empty($value) ) { $i = -1; 869 | 870 | // remove acfcloneindex 871 | if( isset($value['acfcloneindex']) ) { 872 | 873 | unset($value['acfcloneindex']); 874 | 875 | } 876 | 877 | // loop through rows 878 | foreach( $value as $row ) { $i++; 879 | 880 | // bail early if no row 881 | if( !is_array($row) ) continue; 882 | 883 | 884 | // update row 885 | $this->update_row( $row, $i, $field, $post_id ); 886 | 887 | 888 | // append 889 | $new_value++; 890 | 891 | } 892 | 893 | } 894 | 895 | 896 | // remove old rows 897 | if( $old_value > $new_value ) { 898 | 899 | // loop 900 | for( $i = $new_value; $i < $old_value; $i++ ) { 901 | 902 | $this->delete_row( $i, $field, $post_id ); 903 | 904 | } 905 | 906 | } 907 | 908 | 909 | // save false for empty value 910 | if( empty($new_value) ) $new_value = ''; 911 | 912 | 913 | // return 914 | return $new_value; 915 | } 916 | 917 | 918 | /* 919 | * delete_value 920 | * 921 | * description 922 | * 923 | * @type function 924 | * @date 1/07/2015 925 | * @since 5.2.3 926 | * 927 | * @param $post_id (int) 928 | * @return $post_id (int) 929 | */ 930 | 931 | function delete_value( $post_id, $key, $field ) { 932 | 933 | // get old value (db only) 934 | $old_value = (int) acf_get_metadata( $post_id, $field['name'] ); 935 | 936 | 937 | // bail early if no rows or no sub fields 938 | if( !$old_value || empty($field['sub_fields']) ) return; 939 | 940 | 941 | // loop 942 | for( $i = 0; $i < $old_value; $i++ ) { 943 | 944 | $this->delete_row( $i, $field, $post_id ); 945 | 946 | } 947 | 948 | } 949 | 950 | 951 | /* 952 | * delete_field 953 | * 954 | * description 955 | * 956 | * @type function 957 | * @date 4/04/2014 958 | * @since 5.0.0 959 | * 960 | * @param $post_id (int) 961 | * @return $post_id (int) 962 | */ 963 | 964 | function delete_field( $field ) { 965 | 966 | // bail early if no sub fields 967 | if( empty($field['sub_fields']) ) return; 968 | 969 | 970 | // loop through sub fields 971 | foreach( $field['sub_fields'] as $sub_field ) { 972 | 973 | acf_delete_field( $sub_field['ID'] ); 974 | 975 | } 976 | 977 | } 978 | 979 | 980 | /* 981 | * update_field() 982 | * 983 | * This filter is appied to the $field before it is saved to the database 984 | * 985 | * @type filter 986 | * @since 3.6 987 | * @date 23/01/13 988 | * 989 | * @param $field - the field array holding all the field options 990 | * @param $post_id - the field group ID (post_type = acf) 991 | * 992 | * @return $field - the modified field 993 | */ 994 | 995 | function update_field( $field ) { 996 | 997 | // remove sub fields 998 | unset($field['sub_fields']); 999 | 1000 | 1001 | // return 1002 | return $field; 1003 | } 1004 | 1005 | 1006 | /* 1007 | * duplicate_field() 1008 | * 1009 | * This filter is appied to the $field before it is duplicated and saved to the database 1010 | * 1011 | * @type filter 1012 | * @since 3.6 1013 | * @date 23/01/13 1014 | * 1015 | * @param $field - the field array holding all the field options 1016 | * 1017 | * @return $field - the modified field 1018 | */ 1019 | 1020 | function duplicate_field( $field ) { 1021 | 1022 | // get sub fields 1023 | $sub_fields = acf_extract_var( $field, 'sub_fields' ); 1024 | 1025 | 1026 | // save field to get ID 1027 | $field = acf_update_field( $field ); 1028 | 1029 | 1030 | // duplicate sub fields 1031 | acf_duplicate_fields( $sub_fields, $field['ID'] ); 1032 | 1033 | 1034 | // return 1035 | return $field; 1036 | } 1037 | 1038 | 1039 | /* 1040 | * translate_field 1041 | * 1042 | * This function will translate field settings 1043 | * 1044 | * @type function 1045 | * @date 8/03/2016 1046 | * @since 5.3.2 1047 | * 1048 | * @param $field (array) 1049 | * @return $field 1050 | */ 1051 | 1052 | function translate_field( $field ) { 1053 | 1054 | // translate 1055 | $field['button_label'] = acf_translate( $field['button_label'] ); 1056 | 1057 | 1058 | // return 1059 | return $field; 1060 | 1061 | } 1062 | 1063 | 1064 | /* 1065 | * validate_any_field 1066 | * 1067 | * This function will add compatibility for the 'column_width' setting 1068 | * 1069 | * @type function 1070 | * @date 30/1/17 1071 | * @since 5.5.6 1072 | * 1073 | * @param $field (array) 1074 | * @return $field 1075 | */ 1076 | 1077 | function validate_any_field( $field ) { 1078 | 1079 | // width has changed 1080 | if( isset($field['column_width']) ) { 1081 | 1082 | $field['wrapper']['width'] = acf_extract_var($field, 'column_width'); 1083 | 1084 | } 1085 | 1086 | 1087 | // return 1088 | return $field; 1089 | 1090 | } 1091 | 1092 | 1093 | /* 1094 | * prepare_field_for_export 1095 | * 1096 | * description 1097 | * 1098 | * @type function 1099 | * @date 11/03/2014 1100 | * @since 5.0.0 1101 | * 1102 | * @param $post_id (int) 1103 | * @return $post_id (int) 1104 | */ 1105 | 1106 | function prepare_field_for_export( $field ) { 1107 | 1108 | // bail early if no sub fields 1109 | if( empty($field['sub_fields']) ) return $field; 1110 | 1111 | 1112 | // prepare 1113 | $field['sub_fields'] = acf_prepare_fields_for_export( $field['sub_fields'] ); 1114 | 1115 | 1116 | // return 1117 | return $field; 1118 | 1119 | } 1120 | 1121 | 1122 | /* 1123 | * prepare_field_for_import 1124 | * 1125 | * description 1126 | * 1127 | * @type function 1128 | * @date 11/03/2014 1129 | * @since 5.0.0 1130 | * 1131 | * @param $post_id (int) 1132 | * @return $post_id (int) 1133 | */ 1134 | 1135 | function prepare_field_for_import( $field ) { 1136 | 1137 | // bail early if no sub fields 1138 | if( empty($field['sub_fields']) ) return $field; 1139 | 1140 | 1141 | // vars 1142 | $sub_fields = $field['sub_fields']; 1143 | 1144 | 1145 | // reset field setting 1146 | $field['sub_fields'] = array(); 1147 | 1148 | 1149 | // loop 1150 | foreach( $sub_fields as &$sub_field ) { 1151 | 1152 | $sub_field['parent'] = $field['key']; 1153 | 1154 | } 1155 | 1156 | 1157 | // merge 1158 | array_unshift($sub_fields, $field); 1159 | 1160 | 1161 | // return 1162 | return $sub_fields; 1163 | 1164 | } 1165 | 1166 | } 1167 | 1168 | 1169 | // initialize 1170 | acf_register_field_type( 'acf_field_repeater' ); 1171 | 1172 | endif; // class_exists check 1173 | 1174 | ?> -------------------------------------------------------------------------------- /acf-repeater/includes/5-0/field-group.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | 3 | /* 4 | * Repeater 5 | * 6 | * This field type requires some extra logic for its settings 7 | * 8 | * @type function 9 | * @date 24/10/13 10 | * @since 5.0.0 11 | * 12 | * @param n/a 13 | * @return n/a 14 | */ 15 | 16 | var acf_settings_repeater = acf.field_group.field_object.extend({ 17 | 18 | type: 'repeater', 19 | 20 | actions: { 21 | 'render_settings': 'render' 22 | }, 23 | 24 | events: { 25 | 'change .acf-field-setting-layout input': '_change_layout', 26 | 'focus .acf-field-setting-collapsed select': '_focus_collapsed' 27 | }, 28 | 29 | focus: function(){ 30 | 31 | this.$fields = this.$field.find('.acf-field-list:first'); 32 | 33 | }, 34 | 35 | render: function(){ 36 | 37 | this.render_layout(); 38 | this.render_collapsed(); 39 | 40 | }, 41 | 42 | render_layout: function(){ 43 | 44 | // vars 45 | var layout = this.setting('layout input:checked').val(); 46 | 47 | 48 | // update data 49 | this.$fields.attr('data-layout', layout); 50 | 51 | }, 52 | 53 | render_collapsed: function(){ 54 | 55 | // vars 56 | var $select = this.setting('collapsed select'); 57 | 58 | 59 | // collapsed 60 | var choices = []; 61 | 62 | 63 | // keep 'null' choice 64 | choices.push({ 65 | 'label': $select.find('option[value=""]').text(), 66 | 'value': '' 67 | }); 68 | 69 | 70 | // loop 71 | this.$fields.children('.acf-field-object').each(function(){ 72 | 73 | // vars 74 | var $field = $(this); 75 | 76 | 77 | // append 78 | choices.push({ 79 | 'label': $field.find('.field-label:first').val(), 80 | 'value': $field.attr('data-key') 81 | }); 82 | 83 | }); 84 | 85 | 86 | // render 87 | acf.render_select( $select, choices ); 88 | 89 | }, 90 | 91 | _change_layout: function( e ){ 92 | 93 | this.render_layout(); 94 | 95 | }, 96 | 97 | _focus_collapsed: function( e ){ 98 | 99 | this.render_collapsed(); 100 | 101 | } 102 | 103 | }); 104 | 105 | })(jQuery); -------------------------------------------------------------------------------- /acf-repeater/includes/5-0/input.css: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * 3 | * Repeater 4 | * 5 | *---------------------------------------------------------------------------------------------*/ 6 | .acf-repeater { 7 | /* table */ 8 | /* row handle (add/remove) */ 9 | /* add in spacer to th (force correct width) */ 10 | /* row */ 11 | /* sortable */ 12 | /* layouts */ 13 | /* 14 | &.-row > table > tbody > tr:before, 15 | &.-block > table > tbody > tr:before { 16 | content: ""; 17 | display: table-row; 18 | height: 2px; 19 | background: #f00; 20 | } 21 | */ 22 | /* empty */ 23 | /* collapsed */ 24 | /* collapsed (block layout) */ 25 | /* collapsed (table layout) */ 26 | } 27 | .acf-repeater > table { 28 | margin: 0 0 8px; 29 | background: #F9F9F9; 30 | } 31 | .acf-repeater .acf-row-handle { 32 | width: 16px; 33 | text-align: center !important; 34 | vertical-align: middle !important; 35 | position: relative; 36 | /* icons */ 37 | /* .order */ 38 | /* remove */ 39 | } 40 | .acf-repeater .acf-row-handle .acf-icon { 41 | display: none; 42 | position: absolute; 43 | top: 0; 44 | margin: -8px 0 0 -2px; 45 | /* minus icon */ 46 | } 47 | .acf-repeater .acf-row-handle .acf-icon.-minus { 48 | top: 50%; 49 | /* ie fix */ 50 | } 51 | body.browser-msie .acf-repeater .acf-row-handle .acf-icon.-minus { 52 | top: 25px; 53 | } 54 | .acf-repeater .acf-row-handle.order { 55 | background: #f4f4f4; 56 | cursor: move; 57 | color: #aaa; 58 | text-shadow: #fff 0 1px 0; 59 | } 60 | .acf-repeater .acf-row-handle.order:hover { 61 | color: #666; 62 | } 63 | .acf-repeater .acf-row-handle.order + td { 64 | border-left-color: #DFDFDF; 65 | } 66 | .acf-repeater .acf-row-handle.remove { 67 | background: #F9F9F9; 68 | border-left-color: #DFDFDF; 69 | } 70 | .acf-repeater th.acf-row-handle:before { 71 | content: ""; 72 | width: 16px; 73 | display: block; 74 | height: 1px; 75 | } 76 | .acf-repeater .acf-row { 77 | /* hide clone */ 78 | /* hover */ 79 | } 80 | .acf-repeater .acf-row.acf-clone { 81 | display: none !important; 82 | } 83 | .acf-repeater .acf-row:hover, 84 | .acf-repeater .acf-row.-hover { 85 | /* icons */ 86 | } 87 | .acf-repeater .acf-row:hover > .acf-row-handle .acf-icon, 88 | .acf-repeater .acf-row.-hover > .acf-row-handle .acf-icon { 89 | display: block; 90 | } 91 | .acf-repeater > table > tbody > tr.ui-sortable-helper { 92 | box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2); 93 | } 94 | .acf-repeater > table > tbody > tr.ui-sortable-placeholder { 95 | visibility: visible !important; 96 | } 97 | .acf-repeater > table > tbody > tr.ui-sortable-placeholder td { 98 | background: #F9F9F9; 99 | } 100 | .acf-repeater.-row > table > tbody > tr > td, 101 | .acf-repeater.-block > table > tbody > tr > td { 102 | border-top-color: #E1E1E1; 103 | } 104 | .acf-repeater.-empty > table { 105 | border-bottom: 0 none; 106 | } 107 | .acf-repeater.-empty.-row > table, 108 | .acf-repeater.-empty.-block > table { 109 | display: none; 110 | } 111 | .acf-repeater .acf-row.-collapsed > .acf-field { 112 | display: none !important; 113 | } 114 | .acf-repeater .acf-row.-collapsed > td.acf-field.-collapsed-target { 115 | display: table-cell !important; 116 | } 117 | .acf-repeater .acf-row.-collapsed > .acf-fields > * { 118 | display: none !important; 119 | } 120 | .acf-repeater .acf-row.-collapsed > .acf-fields > .acf-field.-collapsed-target { 121 | display: block !important; 122 | } 123 | .acf-repeater.-table .acf-row.-collapsed .acf-field.-collapsed-target { 124 | border-left-color: #dfdfdf; 125 | } -------------------------------------------------------------------------------- /acf-repeater/includes/5-0/input.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | 3 | acf.fields.repeater = acf.field.extend({ 4 | 5 | type: 'repeater', 6 | $el: null, 7 | $input: null, 8 | $table: null, 9 | $tbody: null, 10 | $clone: null, 11 | 12 | actions: { 13 | 'ready': 'initialize', 14 | 'append': 'initialize', 15 | 'show': 'show' 16 | }, 17 | 18 | events: { 19 | 'click a[data-event="add-row"]': '_add', 20 | 'click a[data-event="remove-row"]': '_remove', 21 | 'click a[data-event="collapse-row"]': '_collapse', 22 | 'mouseenter td.order': '_mouseenter' 23 | }, 24 | 25 | focus: function(){ 26 | 27 | // vars 28 | this.$el = this.$field.find('.acf-repeater:first'); 29 | this.$input = this.$field.find('input:first'); 30 | this.$table = this.$field.find('table:first'); 31 | this.$tbody = this.$table.children('tbody'); 32 | this.$clone = this.$tbody.children('tr.acf-clone'); 33 | 34 | 35 | // get options 36 | this.o = acf.get_data(this.$el, { 37 | 'min': 0, 38 | 'max': 0 39 | }); 40 | 41 | 42 | // min / max 43 | this.o.min = parseInt(this.o.min); 44 | this.o.max = parseInt(this.o.max); 45 | 46 | }, 47 | 48 | initialize: function(){ 49 | 50 | // disable clone 51 | acf.disable_form( this.$clone, 'repeater' ); 52 | 53 | 54 | // render 55 | this.render(); 56 | 57 | }, 58 | 59 | show: function(){ 60 | 61 | this.$tbody.find('.acf-field:visible').each(function(){ 62 | 63 | acf.do_action('show_field', $(this)); 64 | 65 | }); 66 | 67 | }, 68 | 69 | count: function(){ 70 | 71 | return this.$tbody.children().length - 1; 72 | 73 | }, 74 | 75 | render: function(){ 76 | 77 | // update order numbers 78 | this.$tbody.children().each(function(i){ 79 | 80 | $(this).find('> td.order > span').html( i+1 ); 81 | 82 | }); 83 | 84 | 85 | // empty? 86 | if( this.count() == 0 ) { 87 | 88 | this.$el.addClass('-empty'); 89 | 90 | } else { 91 | 92 | this.$el.removeClass('-empty'); 93 | 94 | } 95 | 96 | 97 | // row limit reached 98 | if( this.o.max > 0 && this.count() >= this.o.max ) { 99 | 100 | this.$el.find('> .acf-actions .button').addClass('disabled'); 101 | 102 | } else { 103 | 104 | this.$el.find('> .acf-actions .button').removeClass('disabled'); 105 | 106 | } 107 | 108 | }, 109 | 110 | add: function( $tr ){ 111 | 112 | // defaults 113 | $tr = $tr || this.$clone; 114 | 115 | 116 | // validate 117 | if( this.o.max > 0 && this.count() >= this.o.max ) { 118 | 119 | alert( acf._e('repeater','max').replace('{max}', this.o.max) ); 120 | return false; 121 | 122 | } 123 | 124 | 125 | // reference 126 | var $field = this.$field; 127 | 128 | 129 | // duplicate 130 | $el = acf.duplicate( this.$clone ); 131 | 132 | 133 | // remove clone class 134 | $el.removeClass('acf-clone'); 135 | 136 | 137 | // enable 138 | acf.enable_form( $el, 'repeater' ); 139 | 140 | 141 | // move row 142 | $tr.before( $el ); 143 | 144 | 145 | // focus (may have added sub repeater) 146 | this.doFocus($field); 147 | 148 | 149 | // update order 150 | this.render(); 151 | 152 | 153 | // validation 154 | acf.validation.remove_error( this.$field ); 155 | 156 | 157 | // sync collapsed order 158 | this.sync(); 159 | 160 | 161 | // return 162 | return $el; 163 | 164 | }, 165 | 166 | remove: function( $tr ){ 167 | 168 | // reference 169 | var self = this; 170 | 171 | 172 | // validate 173 | if( this.count() <= this.o.min ) { 174 | 175 | alert( acf._e('repeater','min').replace('{min}', this.o.min) ); 176 | return false; 177 | } 178 | 179 | 180 | // action for 3rd party customization 181 | acf.do_action('remove', $tr); 182 | 183 | 184 | // animate out tr 185 | acf.remove_tr( $tr, function(){ 186 | 187 | // trigger change to allow attachment save 188 | self.$input.trigger('change'); 189 | 190 | 191 | // render 192 | self.render(); 193 | 194 | 195 | // sync collapsed order 196 | self.sync(); 197 | 198 | 199 | // refersh field (hide/show columns) 200 | acf.do_action('refresh', self.$field); 201 | 202 | }); 203 | 204 | }, 205 | 206 | sync: function(){ 207 | 208 | // vars 209 | var name = 'collapsed_' + this.$field.data('key'), 210 | collapsed = []; 211 | 212 | 213 | // populate collapsed value 214 | this.$tbody.children().each(function( i ){ 215 | 216 | if( $(this).hasClass('-collapsed') ) { 217 | 218 | collapsed.push( i ); 219 | 220 | } 221 | 222 | }); 223 | 224 | 225 | // update 226 | acf.update_user_setting( name, collapsed.join(',') ); 227 | 228 | }, 229 | 230 | 231 | /* 232 | * events 233 | * 234 | * these functions are fired for this fields events 235 | * 236 | * @type function 237 | * @date 17/09/2015 238 | * @since 5.2.3 239 | * 240 | * @param e 241 | * @return n/a 242 | */ 243 | 244 | _mouseenter: function( e ){ //console.log('_mouseenter'); 245 | 246 | // bail early if already sortable 247 | if( this.$tbody.hasClass('ui-sortable') ) return; 248 | 249 | 250 | // bail early if max 1 row 251 | if( this.o.max == 1 ) return; 252 | 253 | 254 | // reference 255 | var self = this; 256 | 257 | 258 | // add sortable 259 | this.$tbody.sortable({ 260 | items: '> tr', 261 | handle: '> td.order', 262 | forceHelperSize: true, 263 | forcePlaceholderSize: true, 264 | scroll: true, 265 | start: function(event, ui) { 266 | 267 | acf.do_action('sortstart', ui.item, ui.placeholder); 268 | 269 | }, 270 | stop: function(event, ui) { 271 | 272 | // render 273 | self.render(); 274 | 275 | acf.do_action('sortstop', ui.item, ui.placeholder); 276 | 277 | }, 278 | update: function(event, ui) { 279 | 280 | // trigger change 281 | self.$input.trigger('change'); 282 | 283 | } 284 | 285 | }); 286 | 287 | }, 288 | 289 | _add: function( e ){ //console.log('_add'); 290 | 291 | // vars 292 | $row = false; 293 | 294 | 295 | // row add 296 | if( e.$el.hasClass('acf-icon') ) { 297 | 298 | $row = e.$el.closest('.acf-row'); 299 | 300 | } 301 | 302 | 303 | // add 304 | this.add( $row ); 305 | 306 | }, 307 | 308 | _remove: function( e ){ //console.log('_remove'); 309 | 310 | // reference 311 | var self = this; 312 | 313 | 314 | // vars 315 | var $row = e.$el.closest('.acf-row'); 316 | 317 | 318 | // add -open class to show controlls 319 | $row.addClass('-hover'); 320 | 321 | 322 | // confirm 323 | acf.tooltip.confirm_remove( e.$el, function( result ){ 324 | 325 | $row.removeClass('-hover'); 326 | 327 | if( result ) { 328 | 329 | self.remove( $row ); 330 | 331 | } 332 | 333 | }); 334 | 335 | }, 336 | 337 | _collapse: function( e ){ //console.log('_collapse'); 338 | 339 | // vars 340 | var $tr = e.$el.closest('.acf-row'); 341 | 342 | 343 | // reference 344 | var $field = this.$field; 345 | 346 | 347 | // open row 348 | if( $tr.hasClass('-collapsed') ) { 349 | 350 | $tr.removeClass('-collapsed'); 351 | 352 | acf.do_action('show', $tr, 'collapse'); 353 | 354 | } else { 355 | 356 | $tr.addClass('-collapsed'); 357 | 358 | acf.do_action('hide', $tr, 'collapse'); 359 | 360 | } 361 | 362 | 363 | // sync 364 | this.set('$field', $field).sync(); 365 | 366 | 367 | // refersh field (hide/show columns) 368 | acf.do_action('refresh', this.$field); 369 | 370 | } 371 | 372 | }); 373 | 374 | })(jQuery); -------------------------------------------------------------------------------- /acf-repeater/includes/5-7/acf-repeater-field.php: -------------------------------------------------------------------------------- 1 | name = 'repeater'; 25 | $this->label = __("Repeater",'acf'); 26 | $this->category = 'layout'; 27 | $this->defaults = array( 28 | 'sub_fields' => array(), 29 | 'min' => 0, 30 | 'max' => 0, 31 | 'layout' => 'table', 32 | 'button_label' => '', 33 | 'collapsed' => '' 34 | ); 35 | 36 | 37 | // field filters 38 | $this->add_field_filter('acf/prepare_field_for_export', array($this, 'prepare_field_for_export')); 39 | $this->add_field_filter('acf/prepare_field_for_import', array($this, 'prepare_field_for_import')); 40 | 41 | 42 | // filters 43 | $this->add_filter('acf/validate_field',array($this, 'validate_any_field')); 44 | 45 | } 46 | 47 | 48 | /* 49 | * input_admin_enqueue_scripts 50 | * 51 | * description 52 | * 53 | * @type function 54 | * @date 16/12/2015 55 | * @since 5.3.2 56 | * 57 | * @param $post_id (int) 58 | * @return $post_id (int) 59 | */ 60 | 61 | function input_admin_enqueue_scripts() { 62 | 63 | // localize 64 | acf_localize_text(array( 65 | 'Minimum rows reached ({min} rows)' => __('Minimum rows reached ({min} rows)', 'acf'), 66 | 'Maximum rows reached ({max} rows)' => __('Maximum rows reached ({max} rows)', 'acf'), 67 | )); 68 | 69 | wp_enqueue_script( 'acf-input-repeater', acf_get_external_dir(__FILE__, 'input.js'), array('acf-input') ); 70 | wp_enqueue_style( 'acf-input-repeater', acf_get_external_dir(__FILE__, 'input.css'), array('acf-input') ); 71 | 72 | } 73 | 74 | 75 | /* 76 | * field_group_admin_enqueue_scripts 77 | * 78 | * description 79 | * 80 | * @type function 81 | * @date 16/12/2015 82 | * @since 5.3.2 83 | * 84 | * @param $post_id (int) 85 | * @return $post_id (int) 86 | */ 87 | 88 | function field_group_admin_enqueue_scripts() { 89 | 90 | wp_enqueue_script( 'acf-field-group-repeater', acf_get_external_dir(__FILE__, 'field-group.js'), array('acf-field-group') ); 91 | 92 | } 93 | 94 | 95 | /* 96 | * load_field() 97 | * 98 | * This filter is appied to the $field after it is loaded from the database 99 | * 100 | * @type filter 101 | * @since 3.6 102 | * @date 23/01/13 103 | * 104 | * @param $field - the field array holding all the field options 105 | * 106 | * @return $field - the field array holding all the field options 107 | */ 108 | 109 | function load_field( $field ) { 110 | 111 | // min/max 112 | $field['min'] = (int) $field['min']; 113 | $field['max'] = (int) $field['max']; 114 | 115 | 116 | // vars 117 | $sub_fields = acf_get_fields( $field ); 118 | 119 | 120 | // append 121 | if( $sub_fields ) { 122 | 123 | $field['sub_fields'] = $sub_fields; 124 | 125 | } 126 | 127 | 128 | // return 129 | return $field; 130 | 131 | } 132 | 133 | 134 | /* 135 | * render_field() 136 | * 137 | * Create the HTML interface for your field 138 | * 139 | * @param $field - an array holding all the field's data 140 | * 141 | * @type action 142 | * @since 3.6 143 | * @date 23/01/13 144 | */ 145 | 146 | function render_field( $field ) { 147 | 148 | // vars 149 | $sub_fields = $field['sub_fields']; 150 | $show_order = true; 151 | $show_add = true; 152 | $show_remove = true; 153 | 154 | 155 | // bail early if no sub fields 156 | if( empty($sub_fields) ) return; 157 | 158 | 159 | // value 160 | $value = is_array($field['value']) ? $field['value'] : array(); 161 | 162 | 163 | // div 164 | $div = array( 165 | 'class' => 'acf-repeater', 166 | 'data-min' => $field['min'], 167 | 'data-max' => $field['max'] 168 | ); 169 | 170 | 171 | // empty 172 | if( empty($value) ) { 173 | 174 | $div['class'] .= ' -empty'; 175 | 176 | } 177 | 178 | 179 | // If there are less values than min, populate the extra values 180 | if( $field['min'] ) { 181 | 182 | $value = array_pad($value, $field['min'], array()); 183 | 184 | } 185 | 186 | 187 | // If there are more values than man, remove some values 188 | if( $field['max'] ) { 189 | 190 | $value = array_slice($value, 0, $field['max']); 191 | 192 | 193 | // if max 1 row, don't show order 194 | if( $field['max'] == 1 ) { 195 | 196 | $show_order = false; 197 | 198 | } 199 | 200 | 201 | // if max == min, don't show add or remove buttons 202 | if( $field['max'] <= $field['min'] ) { 203 | 204 | $show_remove = false; 205 | $show_add = false; 206 | 207 | } 208 | 209 | } 210 | 211 | 212 | // setup values for row clone 213 | $value['acfcloneindex'] = array(); 214 | 215 | 216 | // button label 217 | if( $field['button_label'] === '' ) $field['button_label'] = __('Add Row', 'acf'); 218 | 219 | 220 | // field wrap 221 | $el = 'td'; 222 | $before_fields = ''; 223 | $after_fields = ''; 224 | 225 | if( $field['layout'] == 'row' ) { 226 | 227 | $el = 'div'; 228 | $before_fields = ''; 229 | $after_fields = ''; 230 | 231 | } elseif( $field['layout'] == 'block' ) { 232 | 233 | $el = 'div'; 234 | 235 | $before_fields = ''; 236 | $after_fields = ''; 237 | 238 | } 239 | 240 | 241 | // layout 242 | $div['class'] .= ' -' . $field['layout']; 243 | 244 | 245 | // collapsed 246 | if( $field['collapsed'] ) { 247 | 248 | // loop 249 | foreach( $sub_fields as &$sub_field ) { 250 | 251 | // add target class 252 | if( $sub_field['key'] == $field['collapsed'] ) { 253 | $sub_field['wrapper']['class'] .= ' -collapsed-target'; 254 | } 255 | } 256 | unset( $sub_field ); 257 | } 258 | 259 | ?> 260 |
> 261 | $field['name'], 'value' => '' )); ?> 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 298 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | $row ): ?> 315 | 316 | 317 | 318 | 324 | 325 | 326 | 327 | 328 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 360 | 361 | 362 | 363 | 364 | 365 |
> 299 | 300 | 301 |

302 | 303 |
319 | 320 | 321 | 322 | 323 | 357 | 358 | 359 |
366 | 367 | 368 |
369 | 370 |
371 | 372 | 373 |
374 | $field['sub_fields'], 397 | 'parent' => $field['ID'] 398 | ); 399 | 400 | 401 | ?> 402 | 403 | 404 |

405 | 406 | 407 | 412 | 413 | 414 | __('Collapsed','acf'), 437 | 'instructions' => __('Select a sub field to show when row is collapsed','acf'), 438 | 'type' => 'select', 439 | 'name' => 'collapsed', 440 | 'allow_null' => 1, 441 | 'choices' => $choices 442 | )); 443 | 444 | 445 | // min 446 | acf_render_field_setting( $field, array( 447 | 'label' => __('Minimum Rows','acf'), 448 | 'instructions' => '', 449 | 'type' => 'number', 450 | 'name' => 'min', 451 | 'placeholder' => '0', 452 | )); 453 | 454 | 455 | // max 456 | acf_render_field_setting( $field, array( 457 | 'label' => __('Maximum Rows','acf'), 458 | 'instructions' => '', 459 | 'type' => 'number', 460 | 'name' => 'max', 461 | 'placeholder' => '0', 462 | )); 463 | 464 | 465 | // layout 466 | acf_render_field_setting( $field, array( 467 | 'label' => __('Layout','acf'), 468 | 'instructions' => '', 469 | 'class' => 'acf-repeater-layout', 470 | 'type' => 'radio', 471 | 'name' => 'layout', 472 | 'layout' => 'horizontal', 473 | 'choices' => array( 474 | 'table' => __('Table','acf'), 475 | 'block' => __('Block','acf'), 476 | 'row' => __('Row','acf') 477 | ) 478 | )); 479 | 480 | 481 | // button_label 482 | acf_render_field_setting( $field, array( 483 | 'label' => __('Button Label','acf'), 484 | 'instructions' => '', 485 | 'type' => 'text', 486 | 'name' => 'button_label', 487 | 'placeholder' => __('Add Row','acf') 488 | )); 489 | 490 | } 491 | 492 | 493 | /* 494 | * load_value() 495 | * 496 | * This filter is applied to the $value after it is loaded from the db 497 | * 498 | * @type filter 499 | * @since 3.6 500 | * @date 23/01/13 501 | * 502 | * @param $value (mixed) the value found in the database 503 | * @param $post_id (mixed) the $post_id from which the value was loaded 504 | * @param $field (array) the field array holding all the field options 505 | * @return $value 506 | */ 507 | 508 | function load_value( $value, $post_id, $field ) { 509 | 510 | // bail early if no value 511 | if( empty($value) ) return false; 512 | 513 | 514 | // bail ealry if not numeric 515 | if( !is_numeric($value) ) return false; 516 | 517 | 518 | // bail early if no sub fields 519 | if( empty($field['sub_fields']) ) return false; 520 | 521 | 522 | // vars 523 | $value = intval($value); 524 | $rows = array(); 525 | 526 | 527 | // loop 528 | for( $i = 0; $i < $value; $i++ ) { 529 | 530 | // create empty array 531 | $rows[ $i ] = array(); 532 | 533 | 534 | // loop through sub fields 535 | foreach( array_keys($field['sub_fields']) as $j ) { 536 | 537 | // get sub field 538 | $sub_field = $field['sub_fields'][ $j ]; 539 | 540 | 541 | // bail ealry if no name (tab) 542 | if( acf_is_empty($sub_field['name']) ) continue; 543 | 544 | 545 | // update $sub_field name 546 | $sub_field['name'] = "{$field['name']}_{$i}_{$sub_field['name']}"; 547 | 548 | 549 | // get value 550 | $sub_value = acf_get_value( $post_id, $sub_field ); 551 | 552 | 553 | // add value 554 | $rows[ $i ][ $sub_field['key'] ] = $sub_value; 555 | 556 | } 557 | 558 | } 559 | 560 | 561 | // return 562 | return $rows; 563 | 564 | } 565 | 566 | 567 | /* 568 | * format_value() 569 | * 570 | * This filter is appied to the $value after it is loaded from the db and before it is returned to the template 571 | * 572 | * @type filter 573 | * @since 3.6 574 | * @date 23/01/13 575 | * 576 | * @param $value (mixed) the value which was loaded from the database 577 | * @param $post_id (mixed) the $post_id from which the value was loaded 578 | * @param $field (array) the field array holding all the field options 579 | * 580 | * @return $value (mixed) the modified value 581 | */ 582 | 583 | function format_value( $value, $post_id, $field ) { 584 | 585 | // bail early if no value 586 | if( empty($value) ) return false; 587 | 588 | 589 | // bail ealry if not array 590 | if( !is_array($value) ) return false; 591 | 592 | 593 | // bail early if no sub fields 594 | if( empty($field['sub_fields']) ) return false; 595 | 596 | 597 | // loop over rows 598 | foreach( array_keys($value) as $i ) { 599 | 600 | // loop through sub fields 601 | foreach( array_keys($field['sub_fields']) as $j ) { 602 | 603 | // get sub field 604 | $sub_field = $field['sub_fields'][ $j ]; 605 | 606 | 607 | // bail ealry if no name (tab) 608 | if( acf_is_empty($sub_field['name']) ) continue; 609 | 610 | 611 | // extract value 612 | $sub_value = acf_extract_var( $value[ $i ], $sub_field['key'] ); 613 | 614 | 615 | // update $sub_field name 616 | $sub_field['name'] = "{$field['name']}_{$i}_{$sub_field['name']}"; 617 | 618 | 619 | // format value 620 | $sub_value = acf_format_value( $sub_value, $post_id, $sub_field ); 621 | 622 | 623 | // append to $row 624 | $value[ $i ][ $sub_field['_name'] ] = $sub_value; 625 | 626 | } 627 | 628 | } 629 | 630 | 631 | // return 632 | return $value; 633 | 634 | } 635 | 636 | 637 | /* 638 | * validate_value 639 | * 640 | * description 641 | * 642 | * @type function 643 | * @date 11/02/2014 644 | * @since 5.0.0 645 | * 646 | * @param $post_id (int) 647 | * @return $post_id (int) 648 | */ 649 | 650 | function validate_value( $valid, $value, $field, $input ){ 651 | 652 | // vars 653 | $count = 0; 654 | 655 | 656 | // check if is value (may be empty string) 657 | if( is_array($value) ) { 658 | 659 | // remove acfcloneindex 660 | if( isset($value['acfcloneindex']) ) { 661 | unset($value['acfcloneindex']); 662 | } 663 | 664 | // count 665 | $count = count($value); 666 | } 667 | 668 | 669 | // validate required 670 | if( $field['required'] && !$count ) { 671 | $valid = false; 672 | } 673 | 674 | 675 | // min 676 | $min = (int) $field['min']; 677 | if( $min && $count < $min ) { 678 | 679 | // create error 680 | $error = __('Minimum rows reached ({min} rows)', 'acf'); 681 | $error = str_replace('{min}', $min, $error); 682 | 683 | // return 684 | return $error; 685 | } 686 | 687 | 688 | // validate value 689 | if( $count ) { 690 | 691 | // bail early if no sub fields 692 | if( !$field['sub_fields'] ) { 693 | return $valid; 694 | } 695 | 696 | // loop rows 697 | foreach( $value as $i => $row ) { 698 | 699 | // loop sub fields 700 | foreach( $field['sub_fields'] as $sub_field ) { 701 | 702 | // vars 703 | $k = $sub_field['key']; 704 | 705 | // test sub field exists 706 | if( !isset($row[ $k ]) ) { 707 | continue; 708 | } 709 | 710 | // validate 711 | acf_validate_value( $row[ $k ], $sub_field, "{$input}[{$i}][{$k}]" ); 712 | } 713 | // end loop sub fields 714 | } 715 | // end loop rows 716 | } 717 | 718 | 719 | // return 720 | return $valid; 721 | } 722 | 723 | 724 | /* 725 | * update_row 726 | * 727 | * This function will update a value row 728 | * 729 | * @type function 730 | * @date 15/2/17 731 | * @since 5.5.8 732 | * 733 | * @param $i (int) 734 | * @param $field (array) 735 | * @param $post_id (mixed) 736 | * @return (boolean) 737 | */ 738 | 739 | function update_row( $row, $field, $post_id, $i = 0 ) { 740 | 741 | // bail early if no layout reference 742 | if( !is_array($row) ) return false; 743 | 744 | 745 | // bail early if no layout 746 | if( empty($field['sub_fields']) ) return false; 747 | 748 | 749 | // loop 750 | foreach( $field['sub_fields'] as $sub_field ) { 751 | 752 | // value 753 | $value = null; 754 | 755 | 756 | // find value (key) 757 | if( isset($row[ $sub_field['key'] ]) ) { 758 | 759 | $value = $row[ $sub_field['key'] ]; 760 | 761 | // find value (name) 762 | } elseif( isset($row[ $sub_field['name'] ]) ) { 763 | 764 | $value = $row[ $sub_field['name'] ]; 765 | 766 | // value does not exist 767 | } else { 768 | 769 | continue; 770 | 771 | } 772 | 773 | 774 | // modify name for save 775 | $sub_field['name'] = "{$field['name']}_{$i}_{$sub_field['name']}"; 776 | 777 | 778 | // update field 779 | acf_update_value( $value, $post_id, $sub_field ); 780 | 781 | } 782 | 783 | 784 | // return 785 | return true; 786 | 787 | } 788 | 789 | 790 | /* 791 | * delete_row 792 | * 793 | * This function will delete a value row 794 | * 795 | * @type function 796 | * @date 15/2/17 797 | * @since 5.5.8 798 | * 799 | * @param $i (int) 800 | * @param $field (array) 801 | * @param $post_id (mixed) 802 | * @return (boolean) 803 | */ 804 | 805 | function delete_row( $field, $post_id, $i = 0 ) { 806 | 807 | // bail early if no sub fields 808 | if( empty($field['sub_fields']) ) return false; 809 | 810 | 811 | // loop 812 | foreach( $field['sub_fields'] as $sub_field ) { 813 | 814 | // modify name for delete 815 | $sub_field['name'] = "{$field['name']}_{$i}_{$sub_field['name']}"; 816 | 817 | 818 | // delete value 819 | acf_delete_value( $post_id, $sub_field ); 820 | 821 | } 822 | 823 | 824 | // return 825 | return true; 826 | 827 | } 828 | 829 | 830 | /* 831 | * update_value() 832 | * 833 | * This filter is appied to the $value before it is updated in the db 834 | * 835 | * @type filter 836 | * @since 3.6 837 | * @date 23/01/13 838 | * 839 | * @param $value - the value which will be saved in the database 840 | * @param $field - the field array holding all the field options 841 | * @param $post_id - the $post_id of which the value will be saved 842 | * 843 | * @return $value - the modified value 844 | */ 845 | 846 | function update_value( $value, $post_id, $field ) { 847 | 848 | // bail early if no sub fields 849 | if( empty($field['sub_fields']) ) return $value; 850 | 851 | 852 | // vars 853 | $new_value = 0; 854 | $old_value = (int) acf_get_metadata( $post_id, $field['name'] ); 855 | 856 | 857 | // update sub fields 858 | if( !empty($value) ) { $i = -1; 859 | 860 | // remove acfcloneindex 861 | if( isset($value['acfcloneindex']) ) { 862 | 863 | unset($value['acfcloneindex']); 864 | 865 | } 866 | 867 | // loop through rows 868 | foreach( $value as $row ) { $i++; 869 | 870 | // bail early if no row 871 | if( !is_array($row) ) continue; 872 | 873 | 874 | // update row 875 | $this->update_row( $row, $field, $post_id, $i ); 876 | 877 | 878 | // append 879 | $new_value++; 880 | 881 | } 882 | 883 | } 884 | 885 | 886 | // remove old rows 887 | if( $old_value > $new_value ) { 888 | 889 | // loop 890 | for( $i = $new_value; $i < $old_value; $i++ ) { 891 | 892 | $this->delete_row( $field, $post_id, $i ); 893 | 894 | } 895 | 896 | } 897 | 898 | 899 | // save false for empty value 900 | if( empty($new_value) ) $new_value = ''; 901 | 902 | 903 | // return 904 | return $new_value; 905 | } 906 | 907 | 908 | /* 909 | * delete_value 910 | * 911 | * description 912 | * 913 | * @type function 914 | * @date 1/07/2015 915 | * @since 5.2.3 916 | * 917 | * @param $post_id (int) 918 | * @return $post_id (int) 919 | */ 920 | 921 | function delete_value( $post_id, $key, $field ) { 922 | 923 | // get old value (db only) 924 | $old_value = (int) acf_get_metadata( $post_id, $field['name'] ); 925 | 926 | 927 | // bail early if no rows or no sub fields 928 | if( !$old_value || empty($field['sub_fields']) ) return; 929 | 930 | 931 | // loop 932 | for( $i = 0; $i < $old_value; $i++ ) { 933 | 934 | $this->delete_row( $field, $post_id, $i ); 935 | 936 | } 937 | 938 | } 939 | 940 | 941 | /* 942 | * delete_field 943 | * 944 | * description 945 | * 946 | * @type function 947 | * @date 4/04/2014 948 | * @since 5.0.0 949 | * 950 | * @param $post_id (int) 951 | * @return $post_id (int) 952 | */ 953 | 954 | function delete_field( $field ) { 955 | 956 | // bail early if no sub fields 957 | if( empty($field['sub_fields']) ) return; 958 | 959 | 960 | // loop through sub fields 961 | foreach( $field['sub_fields'] as $sub_field ) { 962 | 963 | acf_delete_field( $sub_field['ID'] ); 964 | 965 | } 966 | 967 | } 968 | 969 | 970 | /* 971 | * update_field() 972 | * 973 | * This filter is appied to the $field before it is saved to the database 974 | * 975 | * @type filter 976 | * @since 3.6 977 | * @date 23/01/13 978 | * 979 | * @param $field - the field array holding all the field options 980 | * @param $post_id - the field group ID (post_type = acf) 981 | * 982 | * @return $field - the modified field 983 | */ 984 | 985 | function update_field( $field ) { 986 | 987 | // remove sub fields 988 | unset($field['sub_fields']); 989 | 990 | 991 | // return 992 | return $field; 993 | } 994 | 995 | 996 | /* 997 | * duplicate_field() 998 | * 999 | * This filter is appied to the $field before it is duplicated and saved to the database 1000 | * 1001 | * @type filter 1002 | * @since 3.6 1003 | * @date 23/01/13 1004 | * 1005 | * @param $field - the field array holding all the field options 1006 | * 1007 | * @return $field - the modified field 1008 | */ 1009 | 1010 | function duplicate_field( $field ) { 1011 | 1012 | // get sub fields 1013 | $sub_fields = acf_extract_var( $field, 'sub_fields' ); 1014 | 1015 | 1016 | // save field to get ID 1017 | $field = acf_update_field( $field ); 1018 | 1019 | 1020 | // duplicate sub fields 1021 | acf_duplicate_fields( $sub_fields, $field['ID'] ); 1022 | 1023 | 1024 | // return 1025 | return $field; 1026 | } 1027 | 1028 | 1029 | /* 1030 | * translate_field 1031 | * 1032 | * This function will translate field settings 1033 | * 1034 | * @type function 1035 | * @date 8/03/2016 1036 | * @since 5.3.2 1037 | * 1038 | * @param $field (array) 1039 | * @return $field 1040 | */ 1041 | 1042 | function translate_field( $field ) { 1043 | 1044 | // translate 1045 | $field['button_label'] = acf_translate( $field['button_label'] ); 1046 | 1047 | 1048 | // return 1049 | return $field; 1050 | 1051 | } 1052 | 1053 | 1054 | /* 1055 | * validate_any_field 1056 | * 1057 | * This function will add compatibility for the 'column_width' setting 1058 | * 1059 | * @type function 1060 | * @date 30/1/17 1061 | * @since 5.5.6 1062 | * 1063 | * @param $field (array) 1064 | * @return $field 1065 | */ 1066 | 1067 | function validate_any_field( $field ) { 1068 | 1069 | // width has changed 1070 | if( isset($field['column_width']) ) { 1071 | 1072 | $field['wrapper']['width'] = acf_extract_var($field, 'column_width'); 1073 | 1074 | } 1075 | 1076 | 1077 | // return 1078 | return $field; 1079 | 1080 | } 1081 | 1082 | 1083 | /* 1084 | * prepare_field_for_export 1085 | * 1086 | * description 1087 | * 1088 | * @type function 1089 | * @date 11/03/2014 1090 | * @since 5.0.0 1091 | * 1092 | * @param $post_id (int) 1093 | * @return $post_id (int) 1094 | */ 1095 | 1096 | function prepare_field_for_export( $field ) { 1097 | 1098 | // bail early if no sub fields 1099 | if( empty($field['sub_fields']) ) return $field; 1100 | 1101 | 1102 | // prepare 1103 | $field['sub_fields'] = acf_prepare_fields_for_export( $field['sub_fields'] ); 1104 | 1105 | 1106 | // return 1107 | return $field; 1108 | 1109 | } 1110 | 1111 | 1112 | /* 1113 | * prepare_field_for_import 1114 | * 1115 | * description 1116 | * 1117 | * @type function 1118 | * @date 11/03/2014 1119 | * @since 5.0.0 1120 | * 1121 | * @param $post_id (int) 1122 | * @return $post_id (int) 1123 | */ 1124 | 1125 | function prepare_field_for_import( $field ) { 1126 | 1127 | // bail early if no sub fields 1128 | if( empty($field['sub_fields']) ) return $field; 1129 | 1130 | 1131 | // vars 1132 | $sub_fields = $field['sub_fields']; 1133 | 1134 | 1135 | // reset field setting 1136 | $field['sub_fields'] = array(); 1137 | 1138 | 1139 | // loop 1140 | foreach( $sub_fields as &$sub_field ) { 1141 | 1142 | $sub_field['parent'] = $field['key']; 1143 | 1144 | } 1145 | 1146 | 1147 | // merge 1148 | array_unshift($sub_fields, $field); 1149 | 1150 | 1151 | // return 1152 | return $sub_fields; 1153 | 1154 | } 1155 | 1156 | } 1157 | 1158 | 1159 | // initialize 1160 | acf_register_field_type( 'acf_field_repeater' ); 1161 | 1162 | endif; // class_exists check 1163 | 1164 | ?> 1165 | -------------------------------------------------------------------------------- /acf-repeater/includes/5-7/field-group.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | 3 | /* 4 | * Repeater 5 | * 6 | * This field type requires some extra logic for its settings 7 | * 8 | * @type function 9 | * @date 24/10/13 10 | * @since 5.0.0 11 | * 12 | * @param n/a 13 | * @return n/a 14 | */ 15 | 16 | var RepeaterCollapsedFieldSetting = acf.FieldSetting.extend({ 17 | type: 'repeater', 18 | name: 'collapsed', 19 | events: { 20 | 'focus select': 'onFocus', 21 | }, 22 | onFocus: function( e, $el ){ 23 | 24 | // vars 25 | var $select = $el; 26 | 27 | // collapsed 28 | var choices = []; 29 | 30 | // keep 'null' choice 31 | choices.push({ 32 | label: $select.find('option[value=""]').text(), 33 | value: '' 34 | }); 35 | 36 | // find sub fields 37 | var $list = this.fieldObject.$('.acf-field-list:first'); 38 | var fields = acf.getFieldObjects({ 39 | list: $list 40 | }); 41 | 42 | // loop 43 | fields.map(function( field ){ 44 | choices.push({ 45 | label: field.prop('label'), 46 | value: field.prop('key') 47 | }); 48 | }); 49 | 50 | // render 51 | acf.renderSelect( $select, choices ); 52 | } 53 | }); 54 | 55 | acf.registerFieldSetting( RepeaterCollapsedFieldSetting ); 56 | 57 | })(jQuery); -------------------------------------------------------------------------------- /acf-repeater/includes/5-7/input.css: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * 3 | * Repeater 4 | * 5 | *---------------------------------------------------------------------------------------------*/ 6 | .acf-repeater { 7 | /* table */ 8 | /* row handle (add/remove) */ 9 | /* add in spacer to th (force correct width) */ 10 | /* row */ 11 | /* sortable */ 12 | /* layouts */ 13 | /* 14 | &.-row > table > tbody > tr:before, 15 | &.-block > table > tbody > tr:before { 16 | content: ""; 17 | display: table-row; 18 | height: 2px; 19 | background: #f00; 20 | } 21 | */ 22 | /* empty */ 23 | /* collapsed */ 24 | /* collapsed (block layout) */ 25 | /* collapsed (table layout) */ 26 | } 27 | .acf-repeater > table { 28 | margin: 0 0 8px; 29 | background: #F9F9F9; 30 | } 31 | .acf-repeater .acf-row-handle { 32 | width: 16px; 33 | text-align: center !important; 34 | vertical-align: middle !important; 35 | position: relative; 36 | /* icons */ 37 | /* .order */ 38 | /* remove */ 39 | } 40 | .acf-repeater .acf-row-handle .acf-icon { 41 | display: none; 42 | position: absolute; 43 | top: 0; 44 | margin: -8px 0 0 -2px; 45 | /* minus icon */ 46 | } 47 | .acf-repeater .acf-row-handle .acf-icon.-minus { 48 | top: 50%; 49 | /* ie fix */ 50 | } 51 | body.browser-msie .acf-repeater .acf-row-handle .acf-icon.-minus { 52 | top: 25px; 53 | } 54 | .acf-repeater .acf-row-handle.order { 55 | background: #f4f4f4; 56 | cursor: move; 57 | color: #aaa; 58 | text-shadow: #fff 0 1px 0; 59 | } 60 | .acf-repeater .acf-row-handle.order:hover { 61 | color: #666; 62 | } 63 | .acf-repeater .acf-row-handle.order + td { 64 | border-left-color: #DFDFDF; 65 | } 66 | .acf-repeater .acf-row-handle.remove { 67 | background: #F9F9F9; 68 | border-left-color: #DFDFDF; 69 | } 70 | .acf-repeater th.acf-row-handle:before { 71 | content: ""; 72 | width: 16px; 73 | display: block; 74 | height: 1px; 75 | } 76 | .acf-repeater .acf-row { 77 | /* hide clone */ 78 | /* hover */ 79 | } 80 | .acf-repeater .acf-row.acf-clone { 81 | display: none !important; 82 | } 83 | .acf-repeater .acf-row:hover, 84 | .acf-repeater .acf-row.-hover { 85 | /* icons */ 86 | } 87 | .acf-repeater .acf-row:hover > .acf-row-handle .acf-icon, 88 | .acf-repeater .acf-row.-hover > .acf-row-handle .acf-icon { 89 | display: block; 90 | } 91 | .acf-repeater > table > tbody > tr.ui-sortable-helper { 92 | box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2); 93 | } 94 | .acf-repeater > table > tbody > tr.ui-sortable-placeholder { 95 | visibility: visible !important; 96 | } 97 | .acf-repeater > table > tbody > tr.ui-sortable-placeholder td { 98 | background: #F9F9F9; 99 | } 100 | .acf-repeater.-row > table > tbody > tr > td, 101 | .acf-repeater.-block > table > tbody > tr > td { 102 | border-top-color: #E1E1E1; 103 | } 104 | .acf-repeater.-empty > table { 105 | border-bottom: 0 none; 106 | } 107 | .acf-repeater.-empty.-row > table, 108 | .acf-repeater.-empty.-block > table { 109 | display: none; 110 | } 111 | .acf-repeater .acf-row.-collapsed > .acf-field { 112 | display: none !important; 113 | } 114 | .acf-repeater .acf-row.-collapsed > td.acf-field.-collapsed-target { 115 | display: table-cell !important; 116 | } 117 | .acf-repeater .acf-row.-collapsed > .acf-fields > * { 118 | display: none !important; 119 | } 120 | .acf-repeater .acf-row.-collapsed > .acf-fields > .acf-field.-collapsed-target { 121 | display: block !important; 122 | } 123 | .acf-repeater.-table .acf-row.-collapsed .acf-field.-collapsed-target { 124 | border-left-color: #dfdfdf; 125 | } 126 | 127 | /*--------------------------------------------------------------------------------------------- 128 | * 129 | * Sub-fields layout 130 | * 131 | *---------------------------------------------------------------------------------------------*/ 132 | .post-type-acf-field-group #acf-field-group-fields .acf-field-list-wrap .acf-input-sub { 133 | max-width: 100%; 134 | overflow: hidden; 135 | border-radius: 8px; 136 | border-width: 1px; 137 | border-style: solid; 138 | border-color: #dbdfe5; 139 | box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.1); 140 | } 141 | .post-type-acf-field-group #acf-field-group-fields .acf-field-list-wrap .acf-sub-field-list-header { 142 | display: flex; 143 | justify-content: space-between; 144 | align-content: stretch; 145 | align-items: center; 146 | min-height: 64px; 147 | padding-right: 24px; 148 | padding-left: 24px; 149 | } 150 | .post-type-acf-field-group #acf-field-group-fields .acf-field-list-wrap .acf-field-list-wrap { 151 | box-shadow: none; 152 | } 153 | .post-type-acf-field-group #acf-field-group-fields .acf-field-list-wrap .acf-hl.acf-tfoot { 154 | min-height: 64px; 155 | align-items: center; 156 | } 157 | .post-type-acf-field-group #acf-field-group-fields .acf-field-list-wrap .acf-input.acf-input-sub { 158 | max-width: 100%; 159 | margin-right: 0; 160 | margin-left: 0; 161 | } 162 | 163 | .post-type-acf-field-group .acf-field-object .acf-sortable-handle { 164 | width: 100%; 165 | height: 100%; 166 | } 167 | 168 | .post-type-acf-field-group .acf-field-object:hover .acf-sortable-handle:before { 169 | display: none; 170 | } 171 | 172 | .post-type-acf-field-group .acf-field-object:hover .acf-field-list .acf-field-object:hover .acf-sortable-handle:before { 173 | display: block; 174 | } 175 | 176 | .post-type-acf-field-group .acf-field-object .acf-is-subfields .acf-thead .li-field-label:before { 177 | display: none; 178 | } 179 | 180 | .post-type-acf-field-group #acf-field-group-fields .acf-field-list-wrap .acf-input-sub .acf-field-object.open { 181 | border-top-color: #dbdfe5; 182 | } 183 | 184 | .post-type-acf-field-group button.add-layout.acf-btn.acf-btn-primary.add-field, 185 | .post-type-acf-field-group .acf-sub-field-list-header a.acf-btn.acf-btn-secondary.add-field, 186 | .post-type-acf-field-group .acf-field-list-wrap.acf-is-subfields a.acf-btn.acf-btn-secondary.add-field { 187 | height: 32px !important; 188 | min-height: 32px; 189 | margin-left: 5px; 190 | } 191 | 192 | .acf-field-settings-main .acf-field-type-settings { 193 | padding-right: 72px; 194 | padding-left: 72px; 195 | } 196 | -------------------------------------------------------------------------------- /acf-repeater/includes/5-7/input.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | 3 | var Field = acf.Field.extend({ 4 | 5 | type: 'repeater', 6 | wait: '', 7 | 8 | events: { 9 | 'click a[data-event="add-row"]': 'onClickAdd', 10 | 'click a[data-event="remove-row"]': 'onClickRemove', 11 | 'click a[data-event="collapse-row"]': 'onClickCollapse', 12 | 'showField': 'onShow', 13 | 'unloadField': 'onUnload', 14 | 'mouseover': 'onHover', 15 | 'unloadField': 'onUnload' 16 | }, 17 | 18 | $control: function(){ 19 | return this.$('.acf-repeater:first'); 20 | }, 21 | 22 | $table: function(){ 23 | return this.$('table:first'); 24 | }, 25 | 26 | $tbody: function(){ 27 | return this.$('tbody:first'); 28 | }, 29 | 30 | $rows: function(){ 31 | return this.$('tbody:first > tr').not('.acf-clone'); 32 | }, 33 | 34 | $row: function( index ){ 35 | return this.$('tbody:first > tr:eq(' + index + ')'); 36 | }, 37 | 38 | $clone: function(){ 39 | return this.$('tbody:first > tr.acf-clone'); 40 | }, 41 | 42 | $actions: function(){ 43 | return this.$('.acf-actions:last'); 44 | }, 45 | 46 | $button: function(){ 47 | return this.$('.acf-actions:last .button'); 48 | }, 49 | 50 | getValue: function(){ 51 | return this.$rows().length; 52 | }, 53 | 54 | allowRemove: function(){ 55 | var min = parseInt( this.get('min') ); 56 | return ( !min || min < this.val() ); 57 | }, 58 | 59 | allowAdd: function(){ 60 | var max = parseInt( this.get('max') ); 61 | return ( !max || max > this.val() ); 62 | }, 63 | 64 | addSortable: function( self ){ 65 | 66 | // bail early if max 1 row 67 | if( this.get('max') == 1 ) { 68 | return; 69 | } 70 | 71 | // add sortable 72 | this.$tbody().sortable({ 73 | items: '> tr', 74 | handle: '> td.order', 75 | forceHelperSize: true, 76 | forcePlaceholderSize: true, 77 | scroll: true, 78 | stop: function(event, ui) { 79 | self.render(); 80 | }, 81 | update: function(event, ui) { 82 | self.$input().trigger('change'); 83 | } 84 | }); 85 | }, 86 | 87 | addCollapsed: function(){ 88 | 89 | // vars 90 | var indexes = preference.load( this.get('key') ); 91 | 92 | // bail early if no collapsed 93 | if( !indexes ) { 94 | return false; 95 | } 96 | 97 | // loop 98 | this.$rows().each(function( i ){ 99 | if( indexes.indexOf(i) > -1 ) { 100 | $(this).addClass('-collapsed'); 101 | } 102 | }); 103 | }, 104 | 105 | addUnscopedEvents: function( self ){ 106 | 107 | // invalidField 108 | this.on('invalidField', '.acf-row', function(e){ 109 | var $row = $(this); 110 | if( self.isCollapsed($row) ) { 111 | self.expand( $row ); 112 | } 113 | }); 114 | }, 115 | 116 | initialize: function(){ 117 | 118 | // add unscoped events 119 | this.addUnscopedEvents( this ); 120 | 121 | // add collapsed 122 | this.addCollapsed(); 123 | 124 | // disable clone 125 | acf.disable( this.$clone(), this.cid ); 126 | 127 | // render 128 | this.render(); 129 | }, 130 | 131 | render: function(){ 132 | 133 | // update order number 134 | this.$rows().each(function( i ){ 135 | $(this).find('> .order > span').html( i+1 ); 136 | }); 137 | 138 | // empty 139 | if( this.val() == 0 ) { 140 | this.$control().addClass('-empty'); 141 | } else { 142 | this.$control().removeClass('-empty'); 143 | } 144 | 145 | // max 146 | if( this.allowAdd() ) { 147 | this.$button().removeClass('disabled'); 148 | } else { 149 | this.$button().addClass('disabled'); 150 | } 151 | }, 152 | 153 | validateAdd: function(){ 154 | 155 | // return true if allowed 156 | if( this.allowAdd() ) { 157 | return true; 158 | } 159 | 160 | // vars 161 | var max = this.get('max'); 162 | var text = acf.__('Maximum rows reached ({max} rows)'); 163 | 164 | // replace 165 | text = text.replace('{max}', max); 166 | 167 | // add notice 168 | this.showNotice({ 169 | text: text, 170 | type: 'warning' 171 | }); 172 | 173 | // return 174 | return false; 175 | }, 176 | 177 | onClickAdd: function( e, $el ){ 178 | 179 | // validate 180 | if( !this.validateAdd() ) { 181 | return false; 182 | } 183 | 184 | // add above row 185 | if( $el.hasClass('acf-icon') ) { 186 | this.add({ 187 | before: $el.closest('.acf-row') 188 | }); 189 | 190 | // default 191 | } else { 192 | this.add(); 193 | } 194 | }, 195 | 196 | add: function( args ){ 197 | 198 | // validate 199 | if( !this.allowAdd() ) { 200 | return false; 201 | } 202 | 203 | // defaults 204 | args = acf.parseArgs(args, { 205 | before: false 206 | }); 207 | 208 | // add row 209 | var $el = acf.duplicate({ 210 | target: this.$clone(), 211 | append: this.proxy(function( $el, $el2 ){ 212 | 213 | // append 214 | if( args.before ) { 215 | args.before.before( $el2 ); 216 | } else { 217 | $el.before( $el2 ); 218 | } 219 | 220 | // remove clone class 221 | $el2.removeClass('acf-clone'); 222 | 223 | // enable 224 | acf.enable( $el2, this.cid ); 225 | 226 | // render 227 | this.render(); 228 | }) 229 | }); 230 | 231 | // trigger change for validation errors 232 | this.$input().trigger('change'); 233 | 234 | // return 235 | return $el; 236 | }, 237 | 238 | validateRemove: function(){ 239 | 240 | // return true if allowed 241 | if( this.allowRemove() ) { 242 | return true; 243 | } 244 | 245 | // vars 246 | var min = this.get('min'); 247 | var text = acf.__('Minimum rows reached ({min} rows)'); 248 | 249 | // replace 250 | text = text.replace('{min}', min); 251 | 252 | // add notice 253 | this.showNotice({ 254 | text: text, 255 | type: 'warning' 256 | }); 257 | 258 | // return 259 | return false; 260 | }, 261 | 262 | onClickRemove: function( e, $el ){ 263 | 264 | // vars 265 | var $row = $el.closest('.acf-row'); 266 | 267 | // add class 268 | $row.addClass('-hover'); 269 | 270 | // add tooltip 271 | var tooltip = acf.newTooltip({ 272 | confirmRemove: true, 273 | target: $el, 274 | context: this, 275 | confirm: function(){ 276 | this.remove( $row ); 277 | }, 278 | cancel: function(){ 279 | $row.removeClass('-hover'); 280 | } 281 | }); 282 | }, 283 | 284 | remove: function( $row ){ 285 | 286 | // reference 287 | var self = this; 288 | 289 | // remove 290 | acf.remove({ 291 | target: $row, 292 | endHeight: 0, 293 | complete: function(){ 294 | 295 | // trigger change to allow attachment save 296 | self.$input().trigger('change'); 297 | 298 | // render 299 | self.render(); 300 | 301 | // sync collapsed order 302 | //self.sync(); 303 | } 304 | }); 305 | }, 306 | 307 | isCollapsed: function( $row ){ 308 | return $row.hasClass('-collapsed'); 309 | }, 310 | 311 | collapse: function( $row ){ 312 | $row.addClass('-collapsed'); 313 | acf.doAction('hide', $row, 'collapse'); 314 | }, 315 | 316 | expand: function( $row ){ 317 | $row.removeClass('-collapsed'); 318 | acf.doAction('show', $row, 'collapse'); 319 | }, 320 | 321 | onClickCollapse: function( e, $el ){ 322 | 323 | // vars 324 | var $row = $el.closest('.acf-row'); 325 | var isCollpased = this.isCollapsed( $row ); 326 | 327 | // shift 328 | if( e.shiftKey ) { 329 | $row = this.$rows(); 330 | } 331 | 332 | // toggle 333 | if( isCollpased ) { 334 | this.expand( $row ); 335 | } else { 336 | this.collapse( $row ); 337 | } 338 | }, 339 | 340 | onShow: function( e, $el, context ){ 341 | 342 | // get sub fields 343 | var fields = acf.getFields({ 344 | is: ':visible', 345 | parent: this.$el, 346 | }); 347 | 348 | // trigger action 349 | // - ignore context, no need to pass through 'conditional_logic' 350 | // - this is just for fields like google_map to render itself 351 | acf.doAction('show_fields', fields); 352 | }, 353 | 354 | onUnload: function(){ 355 | 356 | // vars 357 | var indexes = []; 358 | 359 | // loop 360 | this.$rows().each(function( i ){ 361 | if( $(this).hasClass('-collapsed') ) { 362 | indexes.push( i ); 363 | } 364 | }); 365 | 366 | // allow null 367 | indexes = indexes.length ? indexes : null; 368 | 369 | // set 370 | preference.save( this.get('key'), indexes ); 371 | }, 372 | 373 | onHover: function(){ 374 | 375 | // add sortable 376 | this.addSortable( this ); 377 | 378 | // remove event 379 | this.off('mouseover'); 380 | } 381 | }); 382 | 383 | acf.registerFieldType( Field ); 384 | 385 | 386 | // register existing conditions 387 | acf.registerConditionForFieldType('hasValue', 'repeater'); 388 | acf.registerConditionForFieldType('hasNoValue', 'repeater'); 389 | acf.registerConditionForFieldType('lessThan', 'repeater'); 390 | acf.registerConditionForFieldType('greaterThan', 'repeater'); 391 | 392 | 393 | // state 394 | var preference = new acf.Model({ 395 | 396 | name: 'this.collapsedRows', 397 | 398 | key: function( key, context ){ 399 | 400 | // vars 401 | var count = this.get(key+context) || 0; 402 | 403 | // update 404 | count++; 405 | this.set(key+context, count, true); 406 | 407 | // modify fieldKey 408 | if( count > 1 ) { 409 | key += '-' + count; 410 | } 411 | 412 | // return 413 | return key; 414 | }, 415 | 416 | load: function( key ){ 417 | 418 | // vars 419 | var key = this.key(key, 'load'); 420 | var data = acf.getPreference(this.name); 421 | 422 | // return 423 | if( data && data[key] ) { 424 | return data[key] 425 | } else { 426 | return false; 427 | } 428 | }, 429 | 430 | save: function( key, value ){ 431 | 432 | // vars 433 | var key = this.key(key, 'save'); 434 | var data = acf.getPreference(this.name) || {}; 435 | 436 | // delete 437 | if( value === null ) { 438 | delete data[ key ]; 439 | 440 | // append 441 | } else { 442 | data[ key ] = value; 443 | } 444 | 445 | // allow null 446 | if( $.isEmptyObject(data) ) { 447 | data = null; 448 | } 449 | 450 | // save 451 | acf.setPreference(this.name, data); 452 | } 453 | }); 454 | 455 | })(jQuery); --------------------------------------------------------------------------------