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  * Abstract base class containing core functionality for Fieldmanager fields.
   5  *
   6  * Fields are UI elements that allow a person to interact with data.
   7  *
   8  * @package Fieldmanager_Field
   9  */
  10 abstract class Fieldmanager_Field {
  11 
  12     /**
  13      * @var boolean
  14      * If true, throw exceptions for illegal behavior
  15      */
  16     public static $debug = FM_DEBUG;
  17 
  18     /**
  19      * @var int
  20      * How many of these fields to display, 0 for no limit
  21      */
  22     public $limit = 1;
  23 
  24     /**
  25      * DEPREATED: How many of these fields to display initially, if $limit != 1.
  26      * @deprecated This argument will have no impact. It only remains to avoid
  27      *             throwing exceptions in code that used it previously.
  28      * @var int
  29      */
  30     public $starting_count = 1;
  31 
  32     /**
  33      * How many of these fields to display at a minimum, if $limit != 1. If
  34      * $limit == $minimum_count, the "add another" button and the remove tool
  35      * will be hidden.
  36      * @var int
  37      */
  38     public $minimum_count = 0;
  39 
  40     /**
  41      * @var int
  42      * How many empty fields to display if $limit != 1 when the total fields in
  43      * the loaded data + $extra_elements > $minimum_count.
  44      */
  45     public $extra_elements = 1;
  46 
  47     /**
  48      * @var string
  49      * Text for add more button
  50      */
  51     public $add_more_label = '';
  52 
  53     /**
  54      * @var string
  55      * The name of the form element, As in 'foo' in <input name="foo" />
  56      */
  57     public $name = '';
  58 
  59     /**
  60      * @var string
  61      * Label to use for form element
  62      */
  63     public $label = '';
  64 
  65     /**
  66      * @var boolean
  67      * If true, the label and the element will display on the same line. Some elements may not support this.
  68      */
  69     public $inline_label = False;
  70 
  71     /**
  72      * @var boolean
  73      * If true, the label will be displayed after the element.
  74      */
  75     public $label_after_element = False;
  76 
  77     /**
  78      * @var string
  79      * Description for the form element
  80      */
  81     public $description = '';
  82 
  83     /**
  84      * @var boolean
  85      * If true, the description will be displayed after the element.
  86      */
  87     public $description_after_element = true;
  88 
  89     /**
  90      * @var string|boolean[]
  91      * Extra HTML attributes to apply to the form element. Use boolean true to apply a standalone attribute, e.g. 'required' => true
  92      */
  93     public $attributes = array();
  94 
  95     /**
  96      * @var string
  97      * CSS class for form element
  98      */
  99     public $field_class = 'element';
 100 
 101     /**
 102      * @var boolean
 103      * Repeat the label for each element if $limit > 1
 104      */
 105     public $one_label_per_item = TRUE;
 106 
 107     /**
 108      * @var boolean
 109      * Allow draggable sorting if $limit > 1
 110      */
 111     public $sortable = FALSE;
 112 
 113     /**
 114      * @var string
 115      * HTML element to use for label
 116      */
 117     public $label_element = 'div';
 118 
 119     /**
 120      * @var callback
 121      * Function to use to sanitize input
 122      */
 123     public $sanitize = 'sanitize_text_field';
 124 
 125     /**
 126      * @var callback[]
 127      * Functions to use to validate input
 128      */
 129     public $validate = array();
 130 
 131     /**
 132      * @var string|array
 133      * jQuery validation rule(s) used to validate this field, entered as a string or associative array.
 134      * These rules will be automatically converted to the appropriate Javascript format.
 135      * For more information see http://jqueryvalidation.org/documentation/
 136      */
 137     public $validation_rules;
 138 
 139     /**
 140      * @var string|array
 141      * jQuery validation messages used by the rule(s) defined for this field, entered as a string or associative array.
 142      * These rules will be automatically converted to the appropriate Javascript format.
 143      * Any messages without a corresponding rule will be ignored.
 144      * For more information see http://jqueryvalidation.org/documentation/
 145      */
 146     public $validation_messages;
 147 
 148     /**
 149      * @var boolean
 150      * Makes the field required on WordPress context forms that already have built-in validation.
 151      * This is necessary only for the fields used with the term add context.
 152      */
 153     public $required = false;
 154 
 155     /**
 156      * @var string|null
 157      * Data type this element is used in, generally set internally
 158      */
 159     public $data_type = NULL;
 160 
 161     /**
 162      * @var int|null
 163      * ID for $this->data_type, eg $post->ID, generally set internally
 164      */
 165     public $data_id = Null;
 166 
 167     /**
 168      * @var boolean
 169      * If true, save empty elements to DB (if $this->limit != 1; single elements are always saved)
 170      */
 171     public $save_empty = False;
 172 
 173     /**
 174      * @var boolean
 175      * Do not save this field (useful for fields which handle saving their own data)
 176      */
 177     public $skip_save = False;
 178 
 179     /**
 180      * Save this field additionally to an index
 181      * @var boolean
 182      */
 183     public $index = False;
 184 
 185     /**
 186      * Save the fields to their own keys (only works in some contexts). Default
 187      * is true.
 188      * @var boolean
 189      */
 190     public $serialize_data = true;
 191 
 192     /**
 193      * @var Fieldmanager_Datasource
 194      * Optionally generate field from datasource. Used by Fieldmanager_Autocomplete and Fieldmanager_Options.
 195      */
 196     public $datasource = Null;
 197 
 198     /**
 199      * @var array[]
 200      * Field name and value on which to display element. Sample:
 201      * $element->display_if = array(
 202      *  'src' => 'display-if-src-element',
 203      *  'value' => 'display-if-src-value'
 204      * );
 205      */
 206     public $display_if = array();
 207 
 208     /**
 209     * @var string
 210     * Where the new item should to added ( top/bottom ) of the stack. Used by Add Another button
 211     * "top|bottom"
 212     */
 213     public $add_more_position = "bottom";
 214 
 215     /**
 216      * @var boolean
 217      * If true, remove any default meta boxes that are overridden by Fieldmanager fields
 218      */
 219     public $remove_default_meta_boxes = False;
 220 
 221     /**
 222      * @var string Template
 223      * The path to the field template
 224      */
 225     public $template = Null;
 226 
 227     /**
 228      * @var array
 229      * If $remove_default_meta_boxes is true, this array will be populated with the list of default meta boxes to remove
 230      */
 231     public $meta_boxes_to_remove = array();
 232 
 233     /**
 234      * @var mixed Default value
 235      * The default value for the field, if unset
 236      */
 237     public $default_value = null;
 238 
 239     /**
 240      * @var callable|null
 241      * Function that parses an index value and returns an optionally modified value.
 242      */
 243     public $index_filter = null;
 244 
 245     /**
 246      * Input type, mainly to support HTML5 input types.
 247      * @var string
 248      */
 249     public $input_type = 'text';
 250 
 251     /**
 252      * Custom escaping for labels, descriptions, etc. Associative array of
 253      * $field => $callable arguments, for example:
 254      *
 255      *     'escape' => array( 'label' => 'wp_kses_post' )
 256      *
 257      * @var array
 258      */
 259     public $escape = array();
 260 
 261     /**
 262      * @var int
 263      * If $this->limit > 1, which element in sequence are we currently rendering?
 264      */
 265     protected $seq = 0;
 266 
 267     /**
 268      * @var boolean
 269      * If $is_proto is true, we're rendering the prototype element for a field that can have infinite instances.
 270      */
 271     protected $is_proto = False;
 272 
 273     /**
 274      * @var Fieldmanager_Field
 275      * Parent element, if applicable. Would be a Fieldmanager_Group unless third-party plugins support this.
 276      */
 277     protected $parent = Null;
 278 
 279     /**
 280      * @todo Add extra wrapper info rather than this specific.
 281      * @var boolean
 282      * Render this element in a tab?
 283      */
 284     protected $is_tab = False;
 285 
 286     /**
 287      * Have we added this field as a meta box yet?
 288      */
 289     private $meta_box_actions_added = False;
 290 
 291     /**
 292      * @var boolean
 293      * Whether or not this field is present on the attachment edit screen
 294      */
 295     public $is_attachment = false;
 296 
 297     /**
 298      * @var int Global Sequence
 299      * The global sequence of elements
 300      */
 301     private static $global_seq = 0;
 302 
 303     /**
 304      * @param mixed string[]|string the value of the element.
 305      * @return string HTML for the element.
 306      * Generate HTML for the form element itself. Generally should be just one tag, no wrappers.
 307      */
 308     public function form_element( $value ) {
 309         if ( !$this->template ) {
 310             $tpl_slug = strtolower( str_replace( 'Fieldmanager_', '', get_class( $this ) ));
 311             $this->template = fieldmanager_get_template( $tpl_slug );
 312         }
 313         ob_start();
 314         include $this->template;
 315         return ob_get_clean();
 316     }
 317 
 318     /**
 319      * Superclass constructor, just populates options and sanity-checks common elements.
 320      * It might also die, but only helpfully, to catch errors in development.
 321      * @param string $label title of form field
 322      * @param array $options with keys matching vars of the field in use.
 323      */
 324     public function __construct( $label = '', $options = array() ) {
 325         $this->set_options( $label, $options );
 326 
 327         // A post can only have one parent, so if this saves to post_parent and
 328         // it's repeatable, we're doing it wrong.
 329         if ( $this->datasource && ! empty( $this->datasource->save_to_post_parent ) && $this->is_repeatable() ) {
 330             _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' );
 331             $this->datasource->save_to_post_parent = false;
 332             $this->datasource->only_save_to_post_parent = false;
 333         }
 334     }
 335 
 336     /**
 337      * Build options into properties and throw errors if developers add an unsupported opt.
 338      * @param string $label title of form field
 339      * @param array $options with keys matching vars of the field in use.
 340      * @throws FM_Developer_Exception if an option is set but not defined in this class or the child class.
 341      * @throws FM_Developer_Exception if an option is set but not public.
 342      */
 343     public function set_options( $label, $options ) {
 344         if ( is_array( $label ) ) {
 345             $options = $label;
 346         } else {
 347             $options['label'] = $label;
 348         }
 349 
 350         // Get all the public properties for this object
 351         $properties = call_user_func( 'get_object_vars', $this );
 352 
 353         foreach ( $options as $key => $value ) {
 354             if ( array_key_exists( $key, $properties ) ) {
 355                 $this->$key = $value;
 356             } elseif ( self::$debug ) {
 357                 $message = sprintf(
 358                     __( 'You attempted to set a property "%1$s" that is nonexistant or invalid for an instance of "%2$s" named "%3$s".', 'fieldmanager' ),
 359                     $key, get_class( $this ), !empty( $options['name'] ) ? $options['name'] : 'NULL'
 360                 );
 361                 throw new FM_Developer_Exception( esc_html( $message ) );
 362             }
 363         }
 364 
 365         // If this is a single field with a limit of 1, serialize_data has no impact
 366         if ( ! $this->serialize_data && ! $this->is_group() && 1 == $this->limit ) {
 367             $this->serialize_data = true;
 368         }
 369 
 370         // Cannot use serialize_data => false with index => true
 371         if ( ! $this->serialize_data && $this->index ) {
 372             throw new FM_Developer_Exception( esc_html__( 'You cannot use `"serialize_data" => false` with `"index" => true`', 'fieldmanager' ) );
 373         }
 374     }
 375 
 376     /**
 377      * Generates all markup needed for all form elements in this field.
 378      * Could be called directly by a plugin or theme.
 379      * @param array $values the current values of this element, in a tree structure if the element has children.
 380      * @return string HTML for all form elements.
 381      */
 382     public function element_markup( $values = array() ) {
 383         $values = $this->preload_alter_values( $values );
 384         if ( $this->limit != 1 ) {
 385             $max = max( $this->minimum_count, count( $values ) + $this->extra_elements );
 386 
 387             // Ensure that we don't display more fields than we can save
 388             if ( $this->limit > 1 && $max > $this->limit ) {
 389                 $max = $this->limit;
 390             }
 391         } else {
 392             $max = 1;
 393         }
 394 
 395         $classes = array( 'fm-wrapper', 'fm-' . $this->name . '-wrapper' );
 396         $fm_wrapper_attrs = array();
 397         if ( $this->sortable ) {
 398             $classes[] = 'fmjs-sortable';
 399         }
 400         $classes = array_merge( $classes, $this->get_extra_element_classes() );
 401 
 402         $out = '';
 403 
 404         // If this element is part of tabbed output, there needs to be a wrapper to contain the tab content
 405         if ( $this->is_tab ) {
 406             $out .= sprintf(
 407                 '<div id="%s-tab" class="wp-tabs-panel"%s>',
 408                 esc_attr( $this->get_element_id() ),
 409                 ( $this->parent->child_count > 0 ) ? ' style="display: none"' : ''
 410             );
 411         }
 412 
 413         // For lists of items where $one_label_per_item = False, the label should go outside the wrapper.
 414         if ( !empty( $this->label ) && !$this->one_label_per_item ) {
 415             $out .= $this->get_element_label( array( 'fm-label-for-list' ) );
 416         }
 417 
 418         // Find the array position of the "counter" (e.g. in element[0], [0] is the counter, thus the position is 1)
 419         $html_array_position = 0; // default is no counter; i.e. if $this->limit = 0
 420         if ( $this->limit != 1 ) {
 421             $html_array_position = 1; // base situation is formname[0], so the counter is in position 1.
 422             if ( $this->parent ) {
 423                 $parent = $this->parent;
 424                 while ( $parent ) {
 425                     $html_array_position++; // one more for having a parent (e.g. parent[this][0])
 426                     if ( $parent->limit != 1 ) { // and another for the parent having multiple (e.g. parent[0][this][0])
 427                         $html_array_position++;
 428                     }
 429                     $parent = $parent->parent; // parent's parent; root element has null parent which breaks while loop.
 430                 }
 431             }
 432         }
 433 
 434         // Checks to see if element has display_if data values, and inserts the data attributes if it does
 435         if ( isset( $this->display_if ) && !empty( $this->display_if ) ) {
 436             $classes[] = 'display-if';
 437             $fm_wrapper_attrs['data-display-src'] = $this->display_if['src'];
 438             $fm_wrapper_attrs['data-display-value'] = $this->display_if['value'];
 439         }
 440         $fm_wrapper_attr_string = '';
 441         foreach ( $fm_wrapper_attrs as $attr => $val ) {
 442             $fm_wrapper_attr_string .= sprintf( '%s="%s" ', sanitize_key( $attr ), esc_attr( $val ) );
 443         }
 444         $out .= sprintf( '<div class="%s" data-fm-array-position="%d" %s>',
 445             esc_attr( implode( ' ', $classes ) ),
 446             absint( $html_array_position ),
 447             $fm_wrapper_attr_string
 448         );
 449 
 450         // After starting the field, apply a filter to allow other plugins to append functionality
 451         $out = apply_filters( 'fm_element_markup_start', $out, $this, $values );
 452         if ( ( 0 == $this->limit || ( $this->limit > 1 && $this->limit > $this->minimum_count ) ) && "top" == $this->add_more_position ) {
 453             $out .= $this->add_another();
 454         }
 455 
 456         if ( 1 != $this->limit ) {
 457             $out .= $this->single_element_markup( null, true );
 458         }
 459         for ( $i = 0; $i < $max; $i++ ) {
 460             $this->seq = $i;
 461             if ( $this->limit == 1 ) {
 462                 $value = $values;
 463             } else {
 464                 $value = isset( $values[ $i ] ) ? $values[ $i ] : Null;
 465             }
 466             $out .= $this->single_element_markup( $value );
 467         }
 468         if ( ( 0 == $this->limit || ( $this->limit > 1 && $this->limit > $this->minimum_count ) ) && "bottom" == $this->add_more_position ) {
 469             $out .= $this->add_another();
 470         }
 471 
 472         // Before closing the field, apply a filter to allow other plugins to append functionality
 473         $out = apply_filters( 'fm_element_markup_end', $out, $this, $values );
 474 
 475         $out .= '</div>';
 476 
 477         // Close the tab wrapper if one exists
 478         if ( $this->is_tab ) $out .= '</div>';
 479 
 480         return $out;
 481     }
 482 
 483     /**
 484      * Generate wrappers and labels for one form element.
 485      * Is called by element_markup(), calls form_element().
 486      * @see Fieldmanager_Field::element_markup()
 487      * @see Fieldmanager_Field::form_element()
 488      * @param mixed $value the current value of this element.
 489      * @param boolean $is_proto true to generate a prototype element for Javascript.
 490      * @return string HTML for a single form element.
 491      */
 492     public function single_element_markup( $value = Null, $is_proto = False ) {
 493         if ( $is_proto ) {
 494             $this->is_proto = true;
 495         }
 496         $out = '';
 497         $classes = array( 'fm-item', 'fm-' . $this->name );
 498 
 499         self::$global_seq++;
 500 
 501         // Drop the fm-group class to hide inner box display if no label is set
 502         if ( !( $this->is_group() && ( !isset( $this->label ) || empty( $this->label ) ) ) ) {
 503             $classes[] = 'fm-' . $this->field_class;
 504         }
 505 
 506         // Check if the required attribute is set. If so, add the class.
 507         if ( $this->required ) {
 508             $classes[] = 'form-required';
 509         }
 510 
 511         if ( $is_proto ) {
 512             $classes[] = 'fmjs-proto';
 513         }
 514 
 515         if ( $this->is_group() && 'vertical' === $this->tabbed ) {
 516             $classes[] = 'fm-tabbed-vertical';
 517         }
 518 
 519         $classes = apply_filters( 'fm_element_classes', $classes, $this->name, $this );
 520 
 521         $out .= sprintf( '<div class="%s">', esc_attr( implode( ' ', $classes ) ) );
 522 
 523         $label = $this->get_element_label( );
 524         $render_label_after = False;
 525         // Hide the label if it is empty or if this is a tab since it would duplicate the title from the tab label
 526         if ( !empty( $this->label ) && !$this->is_tab && $this->one_label_per_item ) {
 527             if ( $this->limit != 1 ) {
 528                 $out .= $this->wrap_with_multi_tools( $label, array( 'fmjs-removable-label' ) );
 529             } elseif ( !$this->label_after_element ) {
 530                 $out .= $label;
 531             } else {
 532                 $render_label_after = True;
 533             }
 534         }
 535 
 536         if ( isset( $this->description ) && !empty( $this->description ) && ! $this->description_after_element ) {
 537             $out .= sprintf( '<div class="fm-item-description">%s</div>', $this->escape( 'description' ) );
 538         }
 539 
 540         if ( Null === $value && Null !== $this->default_value )
 541             $value = $this->default_value;
 542 
 543         $form_element = $this->form_element( $value );
 544 
 545         if ( $this->limit != 1 && ( ! $this->one_label_per_item || empty( $this->label ) ) ) {
 546             $out .= $this->wrap_with_multi_tools( $form_element );
 547         } else {
 548             $out .= $form_element;
 549         }
 550 
 551         if ( $render_label_after ) $out .= $label;
 552 
 553         if ( isset( $this->description ) && !empty( $this->description ) && $this->description_after_element ) {
 554             $out .= sprintf( '<div class="fm-item-description">%s</div>', $this->escape( 'description' ) );
 555         }
 556 
 557         $out .= '</div>';
 558 
 559         if ( $is_proto ) {
 560             $this->is_proto = false;
 561         }
 562         return $out;
 563     }
 564 
 565     /**
 566      * Alter values before rendering
 567      * @param array $values
 568      */
 569     public function preload_alter_values( $values ) {
 570         return apply_filters( 'fm_preload_alter_values', $values, $this );
 571     }
 572 
 573     /**
 574      * Wrap a chunk of HTML with "remove" and "move" buttons if applicable.
 575      * @param string $html HTML to wrap.
 576      * @return string wrapped HTML.
 577      */
 578     public function wrap_with_multi_tools( $html, $classes = array() ) {
 579         $classes[] = 'fmjs-removable';
 580         $out = sprintf( '<div class="%s">', implode( ' ', $classes ) );
 581         if ( $this->sortable ) {
 582             $out .= $this->get_sort_handle();
 583         }
 584         $out .= '<div class="fmjs-removable-element">';
 585         $out .= $html;
 586         $out .= '</div>';
 587 
 588         if ( $this->limit == 0 || $this->limit > $this->minimum_count ) {
 589             $out .= $this->get_remove_handle();
 590         }
 591 
 592         $out .= '</div>';
 593         return $out;
 594     }
 595 
 596     /**
 597      * Get HTML form name (e.g. questions[answer]).
 598      * @return string form name
 599      */
 600     public function get_form_name( $multiple = "" ) {
 601         $tree = $this->get_form_tree();
 602         $name = '';
 603         foreach ( $tree as $level => $branch ) {
 604             if ( 0 == $level ) {
 605                 $name .= $branch->name;
 606             } else {
 607                 $name .= '[' . $branch->name . ']';
 608             }
 609             if ( $branch->limit != 1 ) {
 610                 $name .= '[' . $branch->get_seq() . ']';
 611             }
 612         }
 613         return $name . $multiple;
 614     }
 615 
 616     /**
 617      * Recursively build path to this element (e.g. array(grandparent, parent, this) )
 618      * @return array of parents
 619      */
 620     public function get_form_tree() {
 621         $tree = array();
 622         if ( $this->parent ) {
 623             $tree = $this->parent->get_form_tree();
 624         }
 625         $tree[] = $this;
 626         return $tree;
 627     }
 628 
 629     /**
 630      * Get the ID for the form element itself, uses $this->seq (e.g. which position is this element in).
 631      * Relying on the element's ID for anything isn't a great idea since it can be rewritten in JS.
 632      * @return string ID for use in a form element.
 633      */
 634     public function get_element_id() {
 635         $el = $this;
 636         $id_slugs = array();
 637         while ( $el ) {
 638             $slug = $el->is_proto ? 'proto' : $el->seq;
 639             array_unshift( $id_slugs, $el->name . '-' . $slug );
 640             $el = $el->parent;
 641         }
 642         return 'fm-' . implode( '-', $id_slugs );
 643     }
 644 
 645     /**
 646      * Get the storage key for the form element.
 647      *
 648      * @return string
 649      */
 650     public function get_element_key() {
 651         $el = $this;
 652         $key = $el->name;
 653         while ( $el = $el->parent ) {
 654             if ( $el->add_to_prefix ) {
 655                 $key = "{$el->name}_{$key}";
 656             }
 657         }
 658         return $key;
 659     }
 660 
 661     /**
 662      * Is this element repeatable or does it have a repeatable ancestor?
 663      *
 664      * @return boolean True if yes, false if no.
 665      */
 666     public function is_repeatable() {
 667         if ( 1 != $this->limit ) {
 668             return true;
 669         } elseif ( $this->parent ) {
 670             return $this->parent->is_repeatable();
 671         }
 672         return false;
 673     }
 674 
 675     /**
 676      * Is the current field a group?
 677      *
 678      * @return boolean True if yes, false if no.
 679      */
 680     public function is_group() {
 681         return $this instanceof Fieldmanager_Group;
 682     }
 683 
 684     /**
 685      * Presaves all elements in what could be a set of them, dispatches to $this->presave()
 686      * @input mixed[] $values
 687      * @return mixed[] sanitized values
 688      */
 689     public function presave_all( $values, $current_values ) {
 690         if ( $this->limit == 1 && empty( $this->multiple ) ) {
 691             $values = $this->presave_alter_values( array( $values ), array( $current_values ) );
 692             if ( ! empty( $values ) ) {
 693                 $value = $this->presave( $values[0], $current_values );
 694             } else {
 695                 $value = $values;
 696             }
 697             if ( !empty( $this->index ) ) {
 698                 $this->save_index( array( $value ), array( $current_values ) );
 699             }
 700             return $value;
 701         }
 702 
 703         // If $this->limit != 1, and $values is not an array, that'd just be wrong, and possibly an attack, so...
 704         if ( $this->limit != 1 && !is_array( $values ) ) {
 705 
 706             // EXCEPT maybe this is a request to remove indices
 707             if ( ! empty( $this->index ) && null === $values && ! empty( $current_values ) && is_array( $current_values ) ) {
 708                 $this->save_index( null, $current_values );
 709                 return;
 710             }
 711 
 712             // OR doing cron, where we should just do nothing if there are no values to process.
 713             // OR we've now accumulated some cases where a null value instead of an empty array is an acceptable case to
 714             // just bail out instead of throwing an error. If it WAS an attack, bailing should prevent damage.
 715             if ( null === $values || ( defined( 'DOING_CRON' ) && DOING_CRON && empty( $values ) ) ) {
 716                 return;
 717             }
 718 
 719             $this->_unauthorized_access( sprintf( __( '$values should be an array because $limit is %d', 'fieldmanager' ), $this->limit ) );
 720         }
 721 
 722         if ( empty( $values ) ) {
 723             $values = array();
 724         }
 725 
 726         // Remove the proto
 727         if ( isset( $values['proto'] ) ) {
 728             unset( $values['proto'] );
 729         }
 730 
 731         // If $this->limit is not 0 or 1, and $values has more than $limit, that could also be an attack...
 732         if ( $this->limit > 1 && count( $values ) > $this->limit ) {
 733             $this->_unauthorized_access(
 734                 sprintf( __( 'submitted %1$d values against a limit of %2$d', 'fieldmanager' ), count( $values ), $this->limit )
 735             );
 736         }
 737 
 738         // Check for non-numeric keys
 739         $keys = array_keys( $values );
 740         foreach ( $keys as $key ) {
 741             if ( ! is_numeric( $key ) ) {
 742                 throw new FM_Exception( esc_html__( 'Use of a non-numeric key suggests that something is wrong with this group.', 'fieldmanager' ) );
 743             }
 744         }
 745 
 746         // Condense the array to account for middle items removed
 747         $values = array_values( $values );
 748 
 749         $values = $this->presave_alter_values( $values, $current_values );
 750 
 751         // If this update results in fewer children, trigger presave on empty children to make up the difference.
 752         if ( ! empty( $current_values ) && is_array( $current_values ) ) {
 753             foreach ( array_diff( array_keys( $current_values ), array_keys( $values ) ) as $i ) {
 754                 $values[ $i ] = null;
 755             }
 756         }
 757 
 758         foreach ( $values as $i => $value ) {
 759             $values[ $i ] = $this->presave( $value, empty( $current_values[ $i ] ) ? array() : $current_values[ $i ] );
 760         }
 761 
 762         if ( ! $this->save_empty ) {
 763             // reindex the array after removing empty values
 764             $values = array_values( array_filter( $values ) );
 765         }
 766 
 767         if ( ! empty( $this->index ) ) {
 768             $this->save_index( $values, $current_values );
 769         }
 770 
 771         return $values;
 772     }
 773 
 774     /**
 775      * Optionally save fields to a separate postmeta index for easy lookup with WP_Query
 776      * Handles internal arrays (e.g. for fieldmanager-options).
 777      * Is called multiple times for multi-fields (e.g. limit => 0)
 778      * @param array $values
 779      * @return void
 780      * @todo make this a context method
 781      */
 782     protected function save_index( $values, $current_values ) {
 783         if ( $this->data_type != 'post' || empty( $this->data_id ) ) return;
 784         // Must delete current values specifically, then add new ones, to support a scenario where the
 785         // same field in repeating groups with limit = 1 is going to create more than one entry here, and
 786         // if we called update_post_meta() we would overwrite the index with each new group.
 787         if ( ! empty( $current_values ) && is_array( $current_values ) ) {
 788             foreach ( $current_values as $old_value ) {
 789                 if ( !is_array( $old_value ) ) $old_value = array( $old_value );
 790                 foreach ( $old_value as $value ) {
 791                     $value = $this->process_index_value( $value );
 792                     if ( empty( $value ) ) $value = 0; // false or null should be saved as 0 to prevent duplicates
 793                     delete_post_meta( $this->data_id, $this->index, $value );
 794                 }
 795             }
 796         }
 797         // add new values
 798         if ( ! empty( $values ) && is_array( $values ) ) {
 799             foreach ( $values as $new_value ) {
 800                 if ( !is_array( $new_value ) ) $new_value = array( $new_value );
 801                 foreach ( $new_value as $value ) {
 802                     $value = $this->process_index_value( $value );
 803                     if ( isset( $value ) ) {
 804                         if ( empty( $value ) ) $value = 0; // false or null should be saved as 0 to prevent duplicates
 805                         add_post_meta( $this->data_id, $this->index, $value );
 806                     }
 807                 }
 808             }
 809         }
 810     }
 811 
 812     /**
 813      * Hook to alter handling of an individual index value, which may make sense to change per field type.
 814      * @param mixed $value
 815      * @return mixed
 816      */
 817     protected function process_index_value( $value ) {
 818         if ( is_callable( $this->index_filter ) ) {
 819             $value = call_user_func( $this->index_filter, $value );
 820         }
 821 
 822         return apply_filters( 'fm_process_index_value', $value, $this );
 823     }
 824 
 825     /**
 826      * Hook to alter or respond to all the values of a particular element
 827      * @param array $values
 828      * @return array
 829      */
 830     protected function presave_alter_values( $values, $current_values = array() ) {
 831         return apply_filters( 'fm_presave_alter_values', $values, $this, $current_values );
 832     }
 833 
 834     /**
 835      * Presave function, which handles sanitization and validation
 836      * @param mixed $value If a single field expects to manage an array, it must override presave()
 837      * @return sanitized values.
 838      */
 839     public function presave( $value, $current_value = array() ) {
 840         // It's possible that some elements (Grid is one) would be arrays at
 841         // this point, but those elements must override this function. Let's
 842         // make sure we're dealing with one value here.
 843         if ( is_array( $value ) ) {
 844             $this->_unauthorized_access( __( 'presave() in the base class should not get arrays, but did.', 'fieldmanager' ) );
 845         }
 846         foreach ( $this->validate as $func ) {
 847             if ( !call_user_func( $func, $value ) ) {
 848                 $this->_failed_validation( sprintf(
 849                     __( 'Input "%1$s" is not valid for field "%2$s" ', 'fieldmanager' ),
 850                     (string) $value,
 851                     $this->label
 852                 ) );
 853             }
 854         }
 855         return call_user_func( $this->sanitize, $value );
 856     }
 857 
 858     /**
 859      * Generates an HTML attribute string based on the value of $this->attributes.
 860      * @see Fieldmanager_Field::$attributes
 861      * @return string attributes ready to insert into an HTML tag.
 862      */
 863     public function get_element_attributes() {
 864         $attr_str = array();
 865         foreach ( $this->attributes as $attr => $val ) {
 866             if ( $val === true ){
 867                 $attr_str[] = sanitize_key( $attr );
 868             } else{
 869                 $attr_str[] = sprintf( '%s="%s"', sanitize_key( $attr ), esc_attr( $val ) );
 870             }
 871         }
 872         return implode( ' ', $attr_str );
 873     }
 874 
 875     /**
 876      * Get an HTML label for this element.
 877      * @param array $classes extra CSS classes.
 878      * @return string HTML label.
 879      */
 880     public function get_element_label( $classes = array() ) {
 881         $classes[] = 'fm-label';
 882         $classes[] = 'fm-label-' . $this->name;
 883         if ( $this->inline_label ) {
 884             $this->label_element = 'span';
 885             $classes[] = 'fm-label-inline';
 886         }
 887         if ( $this->label_after_element ) {
 888             $classes[] = 'fm-label-after';
 889         }
 890         return sprintf(
 891             '<%s class="%s"><label for="%s">%s</label></%s>',
 892             sanitize_key( $this->label_element ),
 893             esc_attr( implode( ' ', $classes ) ),
 894             esc_attr( $this->get_element_id( $this->get_seq() ) ),
 895             $this->escape( 'label' ),
 896             sanitize_key( $this->label_element )
 897         );
 898     }
 899 
 900     /**
 901      * Generates HTML for the "Add Another" button.
 902      * @return string button HTML.
 903      */
 904     public function add_another() {
 905         $classes = array( 'fm-add-another', 'fm-' . $this->name . '-add-another', 'button-secondary' );
 906         if ( empty( $this->add_more_label ) ) {
 907             $this->add_more_label = $this->is_group() ? __( 'Add group', 'fieldmanager' ) : __( 'Add field', 'fieldmanager' );
 908         }
 909 
 910         $out = '<div class="fm-add-another-wrapper">';
 911         $out .= sprintf(
 912             '<input type="button" class="%s" value="%s" name="%s" data-related-element="%s" data-add-more-position="%s" data-limit="%d" />',
 913             esc_attr( implode( ' ', $classes ) ),
 914             esc_attr( $this->add_more_label ),
 915             esc_attr( 'fm_add_another_' . $this->name ),
 916             esc_attr( $this->name ),
 917             esc_attr( $this->add_more_position ),
 918             intval( $this->limit )
 919         );
 920         $out .= '</div>';
 921         return $out;
 922     }
 923 
 924     /**
 925      * Return HTML for the sort handle (multi-tools); a separate function to override
 926      * @return string
 927      */
 928     public function get_sort_handle() {
 929         return sprintf( '<div class="fmjs-drag fmjs-drag-icon"><span class="screen-reader-text">%s</span></div>', esc_html__( 'Move', 'fieldmanager' ) );
 930     }
 931 
 932     /**
 933      * Return HTML for the remove handle (multi-tools); a separate function to override
 934      * @return string
 935      */
 936     public function get_remove_handle() {
 937         return sprintf( '<a href="#" class="fmjs-remove" title="%1$s"><span class="screen-reader-text">%1$s</span></a>', esc_attr__( 'Remove', 'fieldmanager' ) );
 938     }
 939 
 940     /**
 941      * Return HTML for the collapse handle (multi-tools); a separate function to override
 942      * @return string
 943      */
 944     public function get_collapse_handle() {
 945         return '<span class="toggle-indicator" aria-hidden="true"></span>';
 946     }
 947 
 948     /**
 949      * Return extra element classes; overriden by some fields.
 950      * @return array
 951      */
 952     public function get_extra_element_classes() {
 953         return array();
 954     }
 955 
 956     /**
 957      * Add a form on user pages
 958      * @param string $title
 959      */
 960     public function add_user_form( $title = '' ) {
 961         $this->require_base();
 962         return new Fieldmanager_Context_User( $title, $this );
 963     }
 964 
 965     /**
 966      * Add a form on a frontend page
 967      * @see Fieldmanager_Context_Form
 968      * @param string $uniqid a unique identifier for this form
 969      */
 970     public function add_page_form( $uniqid ) {
 971         $this->require_base();
 972         return new Fieldmanager_Context_Page( $uniqid, $this );
 973     }
 974 
 975     /**
 976      * Add a form on a term add/edit page
 977      *
 978      * @deprecated 1.0.0-beta.3 Replaced by {@see Fieldmanager_Field::add_term_meta_box()}.
 979      *
 980      * @see Fieldmanager_Context_Term
 981      *
 982      * @param string $title
 983      * @param string|array $taxonomies The taxonomies on which to display this form
 984      * @param boolean $show_on_add Whether or not to show the fields on the add term form
 985      * @param boolean $show_on_edit Whether or not to show the fields on the edit term form
 986      * @param int $parent Only show this field on child terms of this parent term ID
 987      */
 988     public function add_term_form( $title, $taxonomies, $show_on_add = true, $show_on_edit = true, $parent = '' ) {
 989         $this->require_base();
 990         return new Fieldmanager_Context_Term( array(
 991             'title'        => $title,
 992             'taxonomies'   => $taxonomies,
 993             'show_on_add'  => $show_on_add,
 994             'show_on_edit' => $show_on_edit,
 995             'parent'       => $parent,
 996             // Use the deprecated FM Term Meta instead of core's term meta
 997             'use_fm_meta'  => true,
 998             'field'        => $this,
 999         ) );
1000     }
1001 
1002     /**
1003      * Add fields to the term add/edit page
1004      *
1005      * @see Fieldmanager_Context_Term
1006      *
1007      * @param string $title
1008      * @param string|array $taxonomies The taxonomies on which to display this form
1009      * @param boolean $show_on_add Whether or not to show the fields on the add term form
1010      * @param boolean $show_on_edit Whether or not to show the fields on the edit term form
1011      * @param int $parent Only show this field on child terms of this parent term ID
1012      */
1013     public function add_term_meta_box( $title, $taxonomies, $show_on_add = true, $show_on_edit = true, $parent = '' ) {
1014         // Bail if term meta table is not installed.
1015         if ( get_option( 'db_version' ) < 34370 ) {
1016             _doing_it_wrong( __METHOD__, esc_html__( 'This method requires WordPress 4.4 or above', 'fieldmanager' ), 'Fieldmanager-1.0.0-beta.3' );
1017             return false;
1018         }
1019 
1020         $this->require_base();
1021         return new Fieldmanager_Context_Term( array(
1022             'title'        => $title,
1023             'taxonomies'   => $taxonomies,
1024             'show_on_add'  => $show_on_add,
1025             'show_on_edit' => $show_on_edit,
1026             'parent'       => $parent,
1027             'use_fm_meta'  => false,
1028             'field'        => $this,
1029         ) );
1030     }
1031 
1032     /**
1033      * Add this field as a metabox to a post type
1034      * @see Fieldmanager_Context_Post
1035      * @param string $title
1036      * @param string|string[] $post_type
1037      * @param string $context
1038      * @param string $priority
1039      */
1040     public function add_meta_box( $title, $post_types, $context = 'normal', $priority = 'default' ) {
1041         $this->require_base();
1042         // Check if any default meta boxes need to be removed for this field
1043         $this->add_meta_boxes_to_remove( $this->meta_boxes_to_remove );
1044         if ( in_array( 'attachment', (array) $post_types ) ) {
1045             $this->is_attachment = true;
1046         }
1047         return new Fieldmanager_Context_Post( $title, $post_types, $context, $priority, $this );
1048     }
1049 
1050     /**
1051      * Add this field to a post type's quick edit box.
1052      * @see Fieldmanager_Context_Quickedit
1053      * @param string $title
1054      * @param string|string[] $post_type
1055      * @param string $column_title
1056      * @param callable $column_display_callback
1057      */
1058     public function add_quickedit_box( $title, $post_types, $column_display_callback, $column_title = '' ) {
1059         $this->require_base();
1060         return new Fieldmanager_Context_QuickEdit( $title, $post_types, $column_display_callback, $column_title, $this );
1061     }
1062 
1063     /**
1064      * Add this group to an options page
1065      * @param string $title
1066      */
1067     public function add_submenu_page( $parent_slug, $page_title, $menu_title = Null, $capability = 'manage_options', $menu_slug = Null ) {
1068         $this->require_base();
1069         return new Fieldmanager_Context_Submenu( $parent_slug, $page_title, $menu_title, $capability, $menu_slug, $this );
1070     }
1071 
1072     /**
1073      * Activate this group in an already-added submenu page
1074      * @param string $title
1075      */
1076     public function activate_submenu_page() {
1077         $this->require_base();
1078         $submenus = _fieldmanager_registry( 'submenus' );
1079         $s = $submenus[ $this->name ];
1080         $active_submenu = new Fieldmanager_Context_Submenu( $s[0], $s[1], $s[2], $s[3], $s[4], $this, True );
1081         _fieldmanager_registry( 'active_submenu', $active_submenu );
1082     }
1083 
1084     private function require_base() {
1085         if ( !empty( $this->parent ) ) {
1086             throw new FM_Developer_Exception( esc_html__( 'You cannot use this method on a subgroup', 'fieldmanager' ) );
1087         }
1088     }
1089 
1090     /**
1091      * Die violently. If self::$debug is true, throw an exception.
1092      * @param string $debug_message
1093      * @return void e.g. return _you_ into a void.
1094      */
1095     public function _unauthorized_access( $debug_message = '' ) {
1096         if ( self::$debug ) {
1097             throw new FM_Exception( esc_html( $debug_message ) );
1098         }
1099         else {
1100             wp_die( esc_html__( "Sorry, you're not supposed to do that...", 'fieldmanager' ) );
1101         }
1102     }
1103 
1104     /**
1105      * Fail validation. If self::$debug is true, throw an exception.
1106      * @param string $error_message
1107      * @return void
1108      */
1109     protected function _failed_validation( $debug_message = '' ) {
1110         if ( self::$debug ) {
1111             throw new FM_Validation_Exception( $debug_message );
1112         }
1113         else {
1114             wp_die( esc_html(
1115                 $debug_message . "\n\n" .
1116                 __( "You may be able to use your browser's back button to resolve this error.", 'fieldmanager' )
1117             ) );
1118         }
1119     }
1120 
1121     /**
1122      * Die violently. If self::$debug is true, throw an exception.
1123      * @param string $debug_message
1124      * @return void e.g. return _you_ into a void.
1125      */
1126     public function _invalid_definition( $debug_message = '' ) {
1127         if ( self::$debug ) {
1128             throw new FM_Exception( esc_html( $debug_message ) );
1129         } else {
1130             wp_die( esc_html__( "Sorry, you've created an invalid field definition. Please check your code and try again.", 'fieldmanager' ) );
1131         }
1132     }
1133 
1134     /**
1135      * In a multiple element set, return the index of the current element we're rendering.
1136      * @return int
1137      */
1138     protected function get_seq() {
1139         return $this->has_proto() ? 'proto' : $this->seq;
1140     }
1141 
1142     /**
1143      * Are we in the middle of generating a prototype element for repeatable fields?
1144      * @return boolean
1145      */
1146     protected function has_proto() {
1147         if ( $this->is_proto ) return True;
1148         if ( $this->parent ) return $this->parent->has_proto();
1149         return False;
1150     }
1151 
1152     /**
1153      * Helper function to add to the list of meta boxes to remove. This will be defined in child classes that require this functionality.
1154      * @param array current list of meta boxes to remove
1155      * @return void
1156      */
1157     protected function add_meta_boxes_to_remove( &$meta_boxes_to_remove ) {}
1158 
1159     /**
1160      * Escape a field based on the function in the escape argument.
1161      *
1162      * @param  string $field   The field to escape.
1163      * @param  string $default The default function to use to escape the field.
1164      *                         Optional. Defaults to `esc_html()`
1165      * @return string          The escaped field.
1166      */
1167     public function escape( $field, $default = 'esc_html' ) {
1168         if ( isset( $this->escape[ $field ] ) && is_callable( $this->escape[ $field ] ) ) {
1169             return call_user_func( $this->escape[ $field ], $this->$field );
1170         } else {
1171             return call_user_func( $default, $this->$field );
1172         }
1173     }
1174 }
1175 
Fieldmanager API documentation generated by ApiGen 2.8.0