Fieldmanager
  • Package
  • Class
  • Tree
  • Todo

Packages

  • Fieldmanager
    • Context
    • Datasource
    • Field
    • Util
  • None

Classes

  • Fieldmanager_Autocomplete
  • Fieldmanager_Checkbox
  • Fieldmanager_Checkboxes
  • Fieldmanager_Colorpicker
  • Fieldmanager_Datepicker
  • Fieldmanager_DraggablePost
  • Fieldmanager_Field
  • Fieldmanager_Grid
  • Fieldmanager_Group
  • Fieldmanager_Hidden
  • Fieldmanager_Link
  • Fieldmanager_Media
  • Fieldmanager_Options
  • Fieldmanager_Password
  • Fieldmanager_Radios
  • Fieldmanager_RichTextArea
  • Fieldmanager_Select
  • Fieldmanager_TextArea
  • Fieldmanager_TextField
  1 <?php
  2 
  3 /**
  4  * Define a groups of fields.
  5  *
  6  * Groups shouldn't just be thought of as a top-level collection of fields (like
  7  * a meta box). Groups can be infinitely nested, they can be used to create
  8  * tabbed interfaces, and so on. Groups submit data as nested arrays.
  9  *
 10  * @package Fieldmanager_Field
 11  */
 12 class Fieldmanager_Group extends Fieldmanager_Field {
 13 
 14     /**
 15      * @var Fieldmanager_Field[]
 16      * Children elements of this group. Not much point in creating an empty group.
 17      */
 18     public $children = array();
 19 
 20     /**
 21      * @var string
 22      * Override field class
 23      */
 24     public $field_class = 'group';
 25 
 26     /**
 27      * @var string
 28      * Override label element
 29      */
 30     public $label_element = 'h4';
 31 
 32     /**
 33      * @var boolean
 34      * If true, this group can be collapsed by clicking its header.
 35      */
 36     public $collapsible = FALSE;
 37 
 38     /**
 39      * @var boolean
 40      * If true, this group is collapsed by default.
 41      */
 42     public $collapsed = FALSE;
 43 
 44     /**
 45      * Use tabbed groups. Currently supports "horizontal" or "vertical". Default
 46      * is false, which means that the group will not be tabbed.
 47      *
 48      * @var boolean|string
 49      */
 50     public $tabbed = false;
 51 
 52     /**
 53      * @var int
 54      * How many tabs, maximum?
 55      */
 56     public $tab_limit = 0;
 57 
 58     /**
 59      * Persist the active tab on the group between sessions
 60      *
 61      * @var boolean
 62      */
 63     public $persist_active_tab = true;
 64 
 65     /**
 66      * @var array
 67      * Label macro is a more convenient shortcut to label_format and label_token. The first element
 68      * of the two-element array is the title with a placeholder (%s), and the second element is
 69      * simply the name of the child element to pull from, e.g.:
 70      *
 71      * array( 'Section: %s', 'section_title' )
 72      */
 73     public $label_macro = Null;
 74 
 75     /**
 76      * @var string
 77      * If specified, $label_format combined with $label_token will override $label, but only if
 78      * $(label).find(label_token).val() is not null.
 79      */
 80     public $label_format = Null;
 81 
 82     /**
 83      * @var string
 84      * CSS selector to an element to get the token for the label format
 85      */
 86     public $label_token = Null;
 87 
 88     /**
 89      * @var callable|null
 90      * Function that tells whether the group is empty or not. Gets an array of form values.
 91      */
 92     public $group_is_empty = Null;
 93 
 94     /**
 95      * Should the group name be included in the meta key prefix for separate
 96      * fields? Default is true.
 97      *
 98      * If false, Fieldmanager will not check for collisions among the meta keys
 99      * created for this group's fields and other registered fields.
100      *
101      * @var boolean
102      */
103     public $add_to_prefix = true;
104 
105     /**
106      * @var boolean
107      * Iterator value for how many children we have rendered.
108      */
109     protected $child_count = 0;
110 
111     /**
112      * Flag that this field has some descendant with $serialize_data => false.
113      *
114      * This field is set based on its descendants, but you can deliberately set
115      * it yourself if your situation is one where this cannot be determined
116      * automatically (for instance, where descendants are added after the group
117      * has been constructed).
118      *
119      * @var boolean
120      */
121     public $has_unserialized_descendants = false;
122 
123     /**
124      * Constructor; add CSS if we're looking at a tabbed view
125      */
126     public function __construct( $label = '', $options = array() ) {
127 
128         parent::__construct( $label, $options );
129 
130         // Repeatable groups cannot used unserialized data
131         $is_repeatable = ( 1 != $this->limit );
132         if ( ! $this->serialize_data && $is_repeatable ) {
133             throw new FM_Developer_Exception( esc_html__( 'You cannot use `"serialize_data" => false` with repeating groups', 'fieldmanager' ) );
134         }
135 
136         // If this is collapsed, collapsibility is implied
137         if ( $this->collapsed ) {
138             $this->collapsible = True;
139         }
140 
141         // Convenient naming of child elements via their keys
142         foreach ( $this->children as $name => $element ) {
143             // if the array key is not an int, and the name attr is set, and they don't match, we got a problem.
144             if ( $element->name && !is_int( $name ) && $element->name != $name ) {
145                 throw new FM_Developer_Exception( esc_html__( 'Group child name conflict: ', 'fieldmanager' ) . $name . ' / ' . $element->name );
146             } elseif ( ! $element->name ) {
147                 $element->name = $name;
148             }
149 
150             // Catch errors when using serialize_data => false and index => true
151             if ( ! $this->serialize_data && $element->index ) {
152                 throw new FM_Developer_Exception( esc_html__( 'You cannot use `serialize_data => false` with `index => true`', 'fieldmanager' ) );
153             }
154 
155             // A post can only have one parent, so if this saves to post_parent and
156             // it's repeatable, we're doing it wrong.
157             if ( $element->datasource && ! empty( $element->datasource->save_to_post_parent ) && $this->is_repeatable() ) {
158                 _doing_it_wrong( 'Fieldmanager_Datasource_Post::$save_to_post_parent', __( 'A post can only have one parent, therefore you cannot store to post_parent in repeatable fields.', 'fieldmanager' ), '1.0.0' );
159                 $element->datasource->save_to_post_parent = false;
160                 $element->datasource->only_save_to_post_parent = false;
161             }
162 
163             // Flag this group as having unserialized descendants to check invalid use of repeatables
164             if ( ! $this->has_unserialized_descendants && ( ! $element->serialize_data || ( $element->is_group() && $element->has_unserialized_descendants ) ) ) {
165                 $this->has_unserialized_descendants = true;
166             }
167 
168             // Form a child-parent bond
169             $element->parent = $this;
170         }
171 
172         // Check for invalid usage of repeatables and serialize_data
173         if ( $is_repeatable && $this->has_unserialized_descendants ) {
174             throw new FM_Developer_Exception( esc_html__( 'You cannot use `serialize_data => false` with repeating groups', 'fieldmanager' ) );
175         }
176 
177         // Add the tab JS and CSS if it is needed
178         if ( $this->tabbed ) {
179             fm_add_script( 'jquery-hoverintent', 'js/jquery.hoverIntent.js', array( 'jquery' ), '1.8.0' );
180             fm_add_script( 'fm_group_tabs_js', 'js/fieldmanager-group-tabs.js', array( 'jquery', 'jquery-hoverintent' ), '1.0.3' );
181             fm_add_style( 'fm_group_tabs_css', 'css/fieldmanager-group-tabs.css', array(), '1.0.4' );
182         }
183     }
184 
185     /**
186      * Render the element, iterating over children and calling their form_element() functions.
187      * @param mixed $value
188      */
189     public function form_element( $value = NULL ) {
190         $out = '';
191         $tab_group = '';
192         $tab_group_submenu = '';
193 
194         // We do not need the wrapper class for extra padding if no label is set for the group
195         if ( isset( $this->label ) && !empty( $this->label ) ) {
196             $out .= '<div class="fm-group-inner">';
197         }
198 
199         // If the display output for this group is set to tabs, build the tab group for navigation
200         if ( $this->tabbed ) {
201             $tab_group = sprintf( '<ul class="fm-tab-bar wp-tab-bar %s" id="%s-tabs">',
202                 $this->persist_active_tab ? 'fm-persist-active-tab' : '',
203                 esc_attr( $this->get_element_id() ) );
204         }
205 
206         // Produce HTML for each of the children
207         foreach ( $this->children as $element ) {
208 
209             $element->parent = $this;
210 
211             // If the display output for this group is set to tabs, add a tab for this child
212             if ( $this->tabbed ) {
213 
214                 // Set default classes to display the first tab content and hide others
215                 $tab_classes = array( 'fm-tab' );
216                 $tab_classes[] = ( $this->child_count == 0 ) ? "wp-tab-active" : "hide-if-no-js";
217 
218                 // Generate output for the tab. Depends on whether or not there is a tab limit in place.
219                 if ( $this->tab_limit == 0 || $this->child_count < $this->tab_limit ) {
220                     $tab_group .=  sprintf( '<li class="%s"><a href="#%s-tab">%s</a></li>',
221                         esc_attr( implode( " ", $tab_classes ) ),
222                         esc_attr( $element->get_element_id() ),
223                         $element->escape( 'label' )
224                      );
225                 } else if ( $this->tab_limit != 0 && $this->child_count >= $this->tab_limit ) {
226                     $submenu_item_classes = array( 'fm-submenu-item' );
227                     $submenu_item_link_class = "";
228 
229                     // Create the More tab when first hitting the tab limit
230                     if ( $this->child_count == $this->tab_limit ) {
231                         // Create the tab
232                         $tab_group_submenu .=  sprintf( '<li class="fm-tab fm-has-submenu"><a href="#%s-tab">%s</a>',
233                             esc_attr( $element->get_element_id() ),
234                             esc_html__( 'More...', 'fieldmanager' )
235                          );
236 
237                          // Start the submenu
238                          $tab_group_submenu .= sprintf(
239                             '<div class="fm-submenu" id="%s-submenu"><div class="fm-submenu-wrap fm-submenu-wrap"><ul>',
240                             esc_attr( $this->get_element_id() )
241                          );
242 
243                          // Make sure the first submenu item is designated
244                          $submenu_item_classes[] = 'fm-first-item';
245                          $submenu_item_link_class = 'class="fm-first-item"';
246                     }
247 
248                     // Add this element to the More menu
249                     $tab_group_submenu .=  sprintf( '<li class="%s"><a href="#%s-tab" %s>%s</a></li>',
250                         esc_attr( implode( ' ', $submenu_item_classes ) ),
251                         esc_attr( $element->get_element_id() ),
252                         $submenu_item_link_class,
253                         $element->escape( 'label' )
254                     );
255                 }
256 
257                 // Ensure the child is aware it is tab content
258                 $element->is_tab = TRUE;
259             }
260 
261             // Get markup for the child element
262             $child_value = isset( $value[ $element->name ] ) ? $value[ $element->name ] : null;
263 
264             // propagate editor state down the chain
265             if ( $this->data_type ) $element->data_type = $this->data_type;
266             if ( $this->data_id ) $element->data_id = $this->data_id;
267 
268             $out .= $element->element_markup( $child_value );
269 
270             $this->child_count++;
271 
272         }
273 
274         // We do not need the wrapper class for extra padding if no label is set for the group
275         if ( isset( $this->label ) && !empty( $this->label ) ) $out .= '</div>';
276 
277         // If the display output for this group is set to tabs, build the tab group for navigation
278         if ( $this->tab_limit != 0 && $this->child_count >= $this->tab_limit ) $tab_group_submenu .= '</ul></div></div></li>';
279         if ( $this->tabbed ) $tab_group .= $tab_group_submenu . '</ul>';
280 
281 
282         // Return the complete HTML
283         return $tab_group . $out;
284     }
285 
286     /**
287      * Add a child element to this group.
288      * @param Fieldmanager_Field $child
289      * @return void
290      */
291     public function add_child( Fieldmanager_Field $child ) {
292         $child->parent = $this;
293         $this->children[ $child->name ] = $child;
294 
295         // Catch errors when using serialize_data => false and index-> true
296         if ( ! $this->serialize_data && $child->index ) {
297             throw new FM_Developer_Exception( esc_html__( 'You cannot use `serialize_data => false` with `index => true`', 'fieldmanager' ) );
298         }
299     }
300 
301     /**
302      * Presave override for groups which dispatches to child presave_all methods.
303      * @input mixed[] values
304      * @return mixed[] values
305      */
306     public function presave( $values, $current_values = array() ) {
307         // @SECURITY@ First, make sure all the values we're given are legal.
308         if( isset( $values ) && !empty( $values ) ) {
309             foreach ( array_keys( $values ) as $key ) {
310                 if ( !isset( $this->children[$key] ) ) {
311                     // If we're here, it means that the input, generally $_POST, contains a value that doesn't belong,
312                     // and thus one which we cannot sanitize and must not save. This might be an attack.
313                     $this->_unauthorized_access( sprintf( __( 'Found "%1$s" in data but not in children', 'fieldmanager' ), $key ) );
314                 }
315             }
316         }
317 
318         // Then, dispatch them for sanitization to the children.
319         $skip_save_all = true;
320         foreach ( $this->children as $k => $element ) {
321             $element->data_id = $this->data_id;
322             $element->data_type = $this->data_type;
323             if ( ! isset( $values[ $element->name ] ) ) {
324                 $values[ $element->name ] = NULL;
325             }
326 
327             if ( $element->skip_save ) {
328                 unset( $values[ $element->name ] );
329                 continue;
330             }
331 
332             $child_value = empty( $values[ $element->name ] ) ? Null : $values[ $element->name ];
333             $current_child_value = ! isset( $current_values[ $element->name ] ) ? array() : $current_values[ $element->name ];
334             $values[ $element->name ] = $element->presave_all( $values[ $element->name ], $current_child_value );
335             if ( ! $this->save_empty && $this->limit != 1 ) {
336                 if ( is_array( $values[ $element->name ] ) && empty( $values[ $element->name ] ) ) unset( $values[ $element->name ] );
337                 elseif ( empty( $values[ $element->name ] ) ) unset( $values[ $element->name ] );
338             }
339 
340             if ( ! empty( $element->datasource->only_save_to_taxonomy ) || ! empty( $element->datasource->only_save_to_post_parent ) ) {
341                 unset( $values[ $element->name ] );
342                 continue;
343             }
344 
345             $skip_save_all = false;
346         }
347 
348         if ( $skip_save_all ) {
349             $this->skip_save = true;
350         }
351 
352         if ( is_callable( $this->group_is_empty ) ) {
353             if ( call_user_func( $this->group_is_empty, $values ) ) {
354                 $values = array();
355             }
356         }
357 
358         return $values;
359     }
360 
361     /**
362      * Get an HTML label for this element.
363      * @param array $classes extra CSS classes.
364      * @return string HTML label.
365      */
366     public function get_element_label( $classes = array() ) {
367         $classes[] = 'fm-label';
368         $classes[] = 'fm-label-' . $this->name;
369 
370         $wrapper_classes = array( 'fm-group-label-wrapper' );
371 
372         if ( $this->sortable ) {
373             $wrapper_classes[] = 'fmjs-drag';
374             $wrapper_classes[] = 'fmjs-drag-header';
375         }
376 
377         $collapse_handle = '';
378         if ( $this->collapsible ) {
379             $wrapper_classes[] = 'fmjs-collapsible-handle';
380             $collapse_handle = $this->get_collapse_handle();
381         }
382 
383         $extra_attrs = '';
384         if ( $this->label_macro ) {
385             $this->label_format = $this->label_macro[0];
386             $this->label_token = sprintf( '.fm-%s .fm-element:input', $this->label_macro[1] );
387         }
388 
389         if ( $this->label_format && $this->label_token ) {
390             $extra_attrs = sprintf(
391                 'data-label-format="%1$s" data-label-token="%2$s"',
392                 esc_attr( $this->label_format ),
393                 esc_attr( $this->label_token )
394             );
395             $classes[] = 'fm-label-with-macro';
396         }
397 
398         $remove = '';
399         if ( $this->one_label_per_item && ( $this->limit == 0 || ( $this->limit > 1 && $this->limit > $this->minimum_count ) ) ) {
400             $remove = $this->get_remove_handle();
401         }
402 
403         return sprintf(
404             '<div class="%1$s"><%2$s class="%3$s"%4$s>%5$s</%2$s>%6$s%7$s</div>',
405             esc_attr( implode( ' ', $wrapper_classes ) ),
406             $this->label_element,
407             esc_attr( implode( ' ', $classes ) ),
408             $extra_attrs,
409             $this->escape( 'label' ),
410             $collapse_handle,
411             $remove // get_remove_handle() is sanitized html
412         );
413     }
414 
415     /**
416      * Groups have their own drag and remove tools in the label.
417      * @param string $html
418      * @return string
419      */
420     public function wrap_with_multi_tools( $html, $classes = array() ) {
421         if ( empty( $this->label ) || ! $this->one_label_per_item ) {
422             return parent::wrap_with_multi_tools( $html, $classes );
423         }
424         return $html;
425     }
426 
427     /**
428      * Maybe add the collapsible class for groups
429      * @return array
430      */
431     public function get_extra_element_classes() {
432         $classes = array();
433         if ( $this->collapsible ) {
434             $classes[] = 'fm-collapsible';
435         }
436         if ( $this->collapsed ) {
437             $classes[] = 'fm-collapsed';
438         }
439         return $classes;
440     }
441 
442     /**
443      * Helper function to get the list of default meta boxes to remove.
444      * For Fieldmanager_Group, iterate over all children to see if they have meta boxes to remove.
445      * If $remove_default_meta_boxes is true for this group, set all children to also remove any default meta boxes if applicable.
446      * @param $meta_boxes_to_remove the array of meta boxes to remove
447      * @return array list of meta boxes to remove
448      */
449     protected function add_meta_boxes_to_remove( &$meta_boxes_to_remove ) {
450         foreach( $this->children as $child ) {
451             // If remove default meta boxes was true for the group, set it for all children
452             if ( $this->remove_default_meta_boxes ) {
453                 $child->remove_default_meta_boxes = true;
454             }
455 
456             $child->add_meta_boxes_to_remove( $meta_boxes_to_remove );
457         }
458     }
459 
460 }
461 
Fieldmanager API documentation generated by ApiGen 2.8.0