├── 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 |
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 |
547 |
548 |
549 | 1){ echo 'style="display:none;"'; } ?>>
550 |
551 |
552 |
553 |
558 |
559 |
560 |
580 |
581 |
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 | >
310 |
311 |
312 |
313 |
314 | |
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 |
344 |
345 |
346 |
347 |
348 | |
349 |
350 |
351 |
352 |
353 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 | |
393 |
394 |
395 |
396 |
397 |
398 |
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 | >
299 |
300 |
301 |
302 |
303 | |
304 |
305 |
306 |
307 | |
308 |
309 |
310 |
311 |
312 |
313 |
314 | $row ): ?>
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 | |
324 |
325 |
326 |
327 |
328 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 | |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
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);
--------------------------------------------------------------------------------