settings = array( 'version' => ACF_TABLE_FIELD_PLUGIN_VERSION, ); /* * name (string) Single word, no spaces. Underscores allowed */ $this->name = 'table'; $this->description = __('This allows you to easily edit tabular content.', 'advanced-custom-fields-table-field'); $this->preview_image = FileSystem::get('plugin_dir_url') . '/assets/images/field-preview-table.png'; $this->doc_url = 'https://wordpress.org/plugins/advanced-custom-fields-table-field/'; /* * label (string) Multiple words, can include spaces, visible when selecting a field type */ $this->label = __('Table', 'advanced-custom-fields-table-field'); /* * category (string) basic | content | choice | relational | jquery | layout | CUSTOM GROUP NAME */ $this->category = 'content'; /* * defaults (array) Array of default settings which are merged into the field object. These are used later in settings */ $this->defaults = array( //'font_size' => 14, ); /* * l10n (array) Array of strings that are used in JavaScript. This allows JS strings to be translated in PHP and loaded via: * var message = acf._e('table', 'error'); */ $this->l10n = array( //'error' => __('Error! Please enter a higher value.', 'advanced-custom-fields-table-field'), ); // do not delete! parent::__construct(); // PREVENTS SAVING INVALID TABLE FIELD JSON DATA { if ( ! defined( 'ACF_TABLEFIELD_FILTER_POSTMETA' ) OR constant( 'ACF_TABLEFIELD_FILTER_POSTMETA' ) === true ) { add_filter( 'update_post_metadata', function( $x, $object_id, $meta_key, $meta_value, $prev_value ) { // detecting ACF table json if ( is_string( $meta_value ) and strpos( $meta_value, '"acftf":{' ) !== false ) { // is new value a valid json string json_decode( $meta_value ); if ( json_last_error() !== JSON_ERROR_NONE ) { // canceling meta value uptdate $error_message = 'The plugin advanced-custom-fields-table-field prevented a third party update_post_meta( ' . $object_id . ', "' . $meta_key . '", $value ); action that would save a broken JSON string.' . "\n" . 'For details see https://codex.wordpress.org/Function_Reference/update_post_meta#Character_Escaping.'; trigger_error( esc_html( $error_message ), E_USER_WARNING ); return true; } } return $x; }, 10, 5 ); } // } } /* * render_field_settings() * * Create extra settings for your field. These are visible when editing a field * * @type action * @since 3.6 * @date 23/01/13 * * @param $field (array) the $field being edited * @return n/a */ function render_field_settings( $field ) { /* * acf_render_field_setting * * This function will create a setting for your field. Simply pass the $field parameter and an array of field settings. * The array of settings does not require a `value` or `prefix`; These settings are found from the $field array. * * More than one setting can be added by copy/paste the above code. * Please note that you must also have a matching $defaults value for the field name (font_size) */ acf_render_field_setting( $field, array( 'label' => __('Table Header','advanced-custom-fields-table-field'), 'instructions' => __('Presetting the usage of table header','advanced-custom-fields-table-field'), 'type' => 'radio', 'name' => 'use_header', 'choices' => array( 0 => __( "Optional", 'advanced-custom-fields-table-field' ), 1 => __( "Yes", 'advanced-custom-fields-table-field' ), 2 => __( "No", 'advanced-custom-fields-table-field' ), ), 'layout' => 'horizontal', 'default_value' => 0, )); acf_render_field_setting( $field, array( 'label' => __('Table Caption','advanced-custom-fields-table-field'), 'instructions' => __('Presetting the usage of table caption','advanced-custom-fields-table-field'), 'type' => 'radio', 'name' => 'use_caption', 'choices' => array( 1 => __( "Yes", 'advanced-custom-fields-table-field' ), 2 => __( "No", 'advanced-custom-fields-table-field' ), ), 'layout' => 'horizontal', 'default_value' => 2, )); } /* * render_field() * * Create the HTML interface for your field * * @param $field (array) the $field being rendered * * @type action * @since 3.6 * @date 23/01/13 * * @param $field (array) the $field being edited * @return n/a */ function render_field( $field ) { /* * Review the data of $field. * This will show what data is available */ if ( empty( $field['use_header'] ) ) { $field['use_header'] = 0; } if ( empty( $field['use_caption'] ) ) { $field['use_caption'] = 0; } $data_field['use_header'] = $field['use_header']; $data_field['use_caption'] = $field['use_caption']; $e = ''; $e .= '
'; $e .= '
'; // OPTION HEADER { if ( $data_field['use_header'] === 0 ) { $id = uniqid(); $e .= '
'; $e .= ''; $e .= ''; $e .= '
'; } // } // OPTION CAPTION { if ( $data_field['use_caption'] === 1 ) { $id = uniqid(); $e .= '
'; $e .= '
'; $e .= ''; $e .= '
'; } // } $e .= '
'; if ( is_array( $field['value'] ) ) { $field['value'] = wp_json_encode( $field['value'] ); } if ( is_string( $field['value'] ) ) { if ( substr( $field['value'] , 0 , 1 ) === '{' ) { $field['value'] = urlencode( $field['value'] ); } } $e .= '
'; $e .= ''; $e .= '
'; $e .= '
'; echo $e; } /* * input_admin_enqueue_scripts() * * This action is called in the admin_enqueue_scripts action on the edit screen where your field is created. * Use this action to add CSS + JavaScript to assist your render_field() action. * * @type action (admin_enqueue_scripts) * @since 3.6 * @date 23/01/13 * * @param n/a * @return n/a */ function input_admin_enqueue_scripts() { // register & include JS wp_enqueue_script( 'acf-input-table', FileSystem::get('plugin_dir_url') . 'js/init.js', array( 'jquery', 'acf-input' ), $this->settings['version'], true ); wp_enqueue_script( 'acf-input-table-execute', FileSystem::get('plugin_dir_url') . 'js/execute.js', array( 'acf-input-table' ), $this->settings['version'], true ); // register & include CSS wp_register_style( 'acf-input-table', FileSystem::get('plugin_dir_url') . 'css/input.css', array( 'acf-input' ), $this->settings['version'] ); wp_enqueue_style( 'acf-input-table' ); } /* * input_admin_head() * * This action is called in the admin_head action on the edit screen where your field is created. * Use this action to add CSS and JavaScript to assist your render_field() action. * * @type action (admin_head) * @since 3.6 * @date 23/01/13 * * @param n/a * @return n/a */ /* function input_admin_head() { } */ /* * input_form_data() * * This function is called once on the 'input' page between the head and footer * There are 2 situations where ACF did not load during the 'acf/input_admin_enqueue_scripts' and * 'acf/input_admin_head' actions because ACF did not know it was going to be used. These situations are * seen on comments / user edit forms on the front end. This function will always be called, and includes * $args that related to the current screen such as $args['post_id'] * * @type function * @date 6/03/2014 * @since 5.0.0 * * @param $args (array) * @return n/a */ /* function input_form_data( $args ) { } */ /* * input_admin_footer() * * This action is called in the admin_footer action on the edit screen where your field is created. * Use this action to add CSS and JavaScript to assist your render_field() action. * * @type action (admin_footer) * @since 3.6 * @date 23/01/13 * * @param n/a * @return n/a */ /* function input_admin_footer() { } */ /* * field_group_admin_enqueue_scripts() * * This action is called in the admin_enqueue_scripts action on the edit screen where your field is edited. * Use this action to add CSS + JavaScript to assist your render_field_options() action. * * @type action (admin_enqueue_scripts) * @since 3.6 * @date 23/01/13 * * @param n/a * @return n/a */ /* function field_group_admin_enqueue_scripts() { } */ /* * field_group_admin_head() * * This action is called in the admin_head action on the edit screen where your field is edited. * Use this action to add CSS and JavaScript to assist your render_field_options() action. * * @type action (admin_head) * @since 3.6 * @date 23/01/13 * * @param n/a * @return n/a */ /* function field_group_admin_head() { } */ /* * load_value() * * This filter is applied to the $value after it is loaded from the db * * @type filter * @since 3.6 * @date 23/01/13 * * @param $value (mixed) the value found in the database * @param $post_id (mixed) the $post_id from which the value was loaded * @param $field (array) the field array holding all the field options * @return $value */ /* function load_value( $value, $post_id, $field ) { return $value; } */ /* * update_value() * * This filter is applied to the $value before it is saved in the db * * @type filter * @since 3.6 * @date 23/01/13 * * @param $value (mixed) the value found in the database * @param $post_id (mixed) the $post_id from which the value was loaded * @param $field (array) the field array holding all the field options * @return $value */ function update_value( $value, $post_id, $field ) { // DECODES URL ENCODED JSON STRING VALUES { if ( is_string( $value ) ) { $value = wp_unslash( $value ); $value = urldecode( $value ); $value = json_decode( $value, true ); } // } // REJECTS INVALID VALUE TYPES { if ( ! is_array( $value ) ) { return null; } if ( empty( $value ) ) { return null; } // } // GETS FIELD DATA FROM DATABASE { $data = array(); // Gets type of source if ( function_exists( 'acf_decode_post_id' ) ) { $info = acf_decode_post_id( $post_id ); if ( $info['type'] === 'block' ) { // Gets post to parse blocks data $post = get_post(); if ( isset( $post->post_content ) ) { $blocks = parse_blocks( $post->post_content ); if ( ! empty( $blocks ) ) { foreach( $blocks as $block ) { /** * Continue when is no real block * Not just editable blocks are parsed as blocks, but also other post content. * Only real blocks have a blockName. Others have NULL as blockName value. */ if ( ! is_string( $block['blockName'] ) ) { continue; } // Continue when is not a ACF blocks if ( strpos( $block['blockName'], 'acf/' ) !== 0 || empty( $block['attrs'] ) ) { continue; } // Try if ACF block has value if ( isset( $block['attrs']['data'][ $field['name'] ] ) ) { $data = $block['attrs']['data'][ $field['name'] ]; } } } } } else if ( $info['type'] === 'post' || $info['type'] === 'woo_order' ) { $data = get_post_meta( $info['id'], $field['name'], true ); } else if ( $info['type'] === 'user' ) { $data = get_user_meta( $info['id'], $field['name'], true ); } else if ( $info['type'] === 'term' ) { $data = get_term_meta( $info['id'], $field['name'], true ); } else if ( $info['type'] === 'option' ) { $data = get_option('options_' . $field['name']); } } if ( is_string( $data ) ) { $data = json_decode( $data, true ); } if ( ! is_array( $data ) ) { $data = array(); } $data_restore = $data; // } // GET SOME REQUIRED VARS { $db_amount_of_top_cols = 1; $new_amount_of_body_cols = $db_amount_of_top_cols; if ( isset( $data['c'] ) ) { $db_amount_of_top_cols = count( $data['c'] ); } if ( isset( $value['b'][0] ) ) { $new_amount_of_body_cols = count( $value['b'][0] ); } if ( isset( $value['body'][0] ) ) { $new_amount_of_body_cols = count( $value['body'][0] ); } // } // UPDATES FIELD DATA BY UNFORMATED VALUE { if ( isset( $value['p']['o']['uh'] ) ) { $data['p']['o']['uh'] = $value['p']['o']['uh'] ; } if ( isset( $value['p']['ca'] ) ) { $data['p']['ca'] = $value['p']['ca'] ; } if ( isset( $value['h'] ) AND $value['h'] !== false ) { $data['h'] = $value['h']; } if ( isset( $value['b'] ) ) { $data['b'] = $value['b']; } // } // UPDATES FIELD DATA BY FORMATED VALUE { if ( isset( $value['use_header'] ) && $value['use_header'] === true ) { $data['p']['o']['uh'] = 1; } if ( isset( $value['use_header'] ) && $value['use_header'] === false ) { $data['p']['o']['uh'] = 0; } if ( isset( $value['caption'] ) ) { $data['p']['ca'] = $value['caption']; } if ( isset( $value['header'] ) AND $value['header'] !== false ) { $data['h'] = $value['header']; } if ( isset( $value['body'] ) ) { $data['b'] = $value['body']; } // } // APPLIES CHANGES TO VALUE { $value = $data; // } // SYNCHRONICE TOP ROW DATA WITH CHANGED AMOUNT OF BODY COLUMNS { if ( $new_amount_of_body_cols > $db_amount_of_top_cols ) { for ( $i = $db_amount_of_top_cols; $i < $new_amount_of_body_cols; $i++ ) { // adds a column entry in top row data $value['c'] = $value['c'] ?? array(); array_push( $value['c'], array( 'p' => '' ) ); } } if ( $new_amount_of_body_cols < $db_amount_of_top_cols ) { for ( $i = $new_amount_of_body_cols; $i < $db_amount_of_top_cols; $i++ ) { // removes a column entry in top row data array_pop( $value['c'] ); } } // } // UPDATES PLUGIN VERSION { $value['acftf']['v'] = ACF_TABLE_FIELD_PLUGIN_VERSION; // } // ENSURES ORDERING ROWS AND COLS BY GIVEN KEYS { // ensures ordering of header row data if ( isset( $value['h'] ) ) { ksort( $value['h'], SORT_NUMERIC ); } // ensures ordering of body rows and cols if ( isset( $value['b'] ) ) { ksort( $value['b'], SORT_NUMERIC ); foreach ( $value['b'] as &$row_data ) { ksort( $row_data, SORT_NUMERIC ); } unset( $row_data ); } // } // APPLIES DEFAULT VALUES BY SCHEMA { $schema = $this->get_rest_schema( $field ); $value = $this->apply_schema_defaults( $value, $schema ); // } // STORES REST VALIDATION RESULT { $validation_before_sanitize = rest_validate_value_from_schema( $value, $schema ); // } // SANITIZES VALUE BY REST SCHEMA { $value = rest_sanitize_value_from_schema( $value, $schema ); // } // SANITIZES STRING VALUES { array_walk_recursive( $value, function ( &$item ) { if ( is_string( $item ) ) { $item = wp_kses_post( $item ); } }); // } // VALIDATES VALUE BY REST SCHEMA { $sanitized_validation = rest_validate_value_from_schema( $value, $schema ); if ( is_wp_error( $sanitized_validation ) ) { if ( is_wp_error( $validation_before_sanitize ) ) { error_log( print_r( $validation_before_sanitize, true ) ); } else { error_log( print_r( $sanitized_validation, true ) ); } return $data_restore; } // } // $post_id is integer when post is saved, $post_id is string when block is saved /* if ( gettype( $post_id ) === 'integer' ) { // only saving a post needs addslashes $value = $this->table_slash( $value ); } */ return $value; } /* * format_value() * * This filter is appied to the $value after it is loaded from the db and before it is returned to the template * * @type filter * @since 3.6 * @date 23/01/13 * * @param $value (mixed) the value which was loaded from the database * @param $post_id (mixed) the $post_id from which the value was loaded * @param $field (array) the field array holding all the field options * * @return $value (mixed) the modified value */ function format_value( $value, $post_id, $field ) { if ( is_string( $value ) ) { // CHECK FOR GUTENBERG BLOCK CONTENT (URL ENCODED JSON) { if ( substr( $value , 0 , 1 ) === '%' ) { $value = urldecode( $value ); } // } $value = json_decode( $value, true ); // decode gutenberg JSONs, but also old table JSONs strings to array } $a = $value; $value = false; // IF BODY DATA if ( ! empty( $a['b'] ) AND is_countable( $a['b'] ) AND count( $a['b'] ) > 0 ) { $value = array(); // IF HEADER DATA if ( isset( $a['p']['o']['uh'] ) ) { if ( 0 === $a['p']['o']['uh'] ) { $value['use_header'] = false; } elseif ( 1 === $a['p']['o']['uh'] ) { $value['use_header'] = true; } } if ( ! empty( $a['p']['o']['uh'] ) ) { $value['header'] = $a['h']; } else { $value['header'] = false; } // IF CAPTION DATA if ( ! empty( $field['use_caption'] ) AND $field['use_caption'] === 1 AND ! empty( $a['p']['ca'] ) ) { $value['caption'] = $a['p']['ca']; } else { $value['caption'] = false; } // BODY $value['body'] = $a['b']; // IF SINGLE EMPTY CELL, THEN DO NOT RETURN TABLE DATA if ( count( $a['b'] ) === 1 AND count( $a['b'][0] ) === 1 AND trim( $a['b'][0][0]['c'] ) === '' ) { $value = false; } } return $value; } /* * validate_value() * * This filter is used to perform validation on the value prior to saving. * All values are validated regardless of the field's required setting. This allows you to validate and return * messages to the user if the value is not correct * * @type filter * @date 11/02/2014 * @since 5.0.0 * * @param $valid (boolean) validation status based on the value and the field's required setting * @param $value (mixed) the $_POST value * @param $field (array) the field array holding all the field options * @param $input (string) the corresponding input name for $_POST value * @return $valid */ /* function validate_value( $valid, $value, $field, $input ){ // Basic usage if( $value < $field['custom_minimum_setting'] ) { $valid = false; } // Advanced usage if( $value < $field['custom_minimum_setting'] ) { $valid = __('The value is too little!','advanced-custom-fields-table-field'), } // return return $valid; } */ /* * delete_value() * * This action is fired after a value has been deleted from the db. * Please note that saving a blank value is treated as an update, not a delete * * @type action * @date 6/03/2014 * @since 5.0.0 * * @param $post_id (mixed) the $post_id from which the value was deleted * @param $key (string) the $meta_key which the value was deleted * @return n/a */ /* function delete_value( $post_id, $key ) { } */ /* * load_field() * * This filter is applied to the $field after it is loaded from the database * * @type filter * @date 23/01/2013 * @since 3.6.0 * * @param $field (array) the field array holding all the field options * @return $field */ /* function load_field( $field ) { return $field; } */ /* * update_field() * * This filter is applied to the $field before it is saved to the database * * @type filter * @date 23/01/2013 * @since 3.6.0 * * @param $field (array) the field array holding all the field options * @return $field */ /* function update_field( $field ) { return $field; } */ /* * delete_field() * * This action is fired after a field is deleted from the database * * @type action * @date 11/02/2014 * @since 5.0.0 * * @param $field (array) the field array holding all the field options * @return n/a */ /* function delete_field( $field ) { } */ /** * get_rest_schema() * * Provides the rest api scheme. */ public function get_rest_schema( array $field ) { $schema = array( 'type' => array( 'object', 'null' ), 'title' => 'ACF table custom field type', 'properties' => array( 'acftf' => array( 'title' => 'Table general info', 'type' => 'object', 'properties' => array( 'v' => array( 'title' => 'Table Field plugin version', 'type' => 'string', 'required' => true, 'default' => ACF_TABLE_FIELD_PLUGIN_VERSION, ), ), 'additionalProperties' => false, 'required' => true, 'default' => array( 'v' => ACF_TABLE_FIELD_PLUGIN_VERSION ), ), 'p' => array( 'title' => 'Field parameter', 'type' => 'object', 'properties' => array( 'o' => array( 'title' => 'Table options', 'type' => 'object', 'properties' => array( 'uh' => array( 'title' => 'Use table header', 'type' => 'integer', 'enum' => array( 0, 1 ), 'required' => true, 'default' => 0, ), ), 'additionalProperties' => false, 'default' => array( 'uh' => 0 ), 'required' => true, ), 'ca' => array( 'title' => 'Table caption', 'type' => 'string', 'required' => true, 'default' => '', ), ), 'additionalProperties' => false, 'required' => true, 'default' => array( 'o' => array( 'uh' => 0 ), 'ca' => '' ), ), 'c' => array( 'title' => 'Table columns', 'type' => 'array', 'items' => array( 'title' => 'Table column', 'type' => 'object', 'properties' => array( 'p' => array( 'title' => 'Column parameters', 'type' => 'object', 'required' => true, 'default' => array(), ), ), 'additionalProperties' => false, 'default' => array( 'p' => array() ), 'required' => true, ), 'required' => true, 'default' => array( array( 'p' => '' ) ), ), 'h' => array( 'title' => 'Table header rows', 'type' => 'array', 'items' => array( 'title' => 'Table header cell', 'type' => 'object', 'properties' => array( 'c' => array( 'title' => 'Header cell content', 'type' => 'string', 'required' => true, 'default' => '', ), ), 'additionalProperties' => false, 'default' => array( 'c' => '' ), ), 'required' => true, 'default' => array( array( 'c' => '' ) ), ), 'b' => array( 'title' => 'Table body rows', 'type' => 'array', 'items' => array( 'title' => 'Table body columns', 'type' => 'array', 'items' => array( 'title' => 'Table body cell', 'type' => 'object', 'properties' => array( 'c' => array( 'title' => 'Header row cells', 'type' => 'string', 'required' => true, 'default' => '', ), ), 'additionalProperties' => false, 'default' => array( 'c' => '' ), ), 'required' => true, 'default' => array( array( 'c' => '' ) ), ), 'required' => true, 'default' => array( array( array( 'c' => '' ) ) ), ), ), 'additionalProperties' => false, 'required' => ! empty( $field['required'] ) ? true : false, ); return $schema; } /** * apply_schema_defaults() * * Applies default value entries given by schema */ function apply_schema_defaults( $value, array $schema ) { if ( ( null === $value || array() === $value ) && isset( $schema['default'] ) ) { $value = $schema['default']; } $schema_types = array(); if ( is_array( $schema['type'] ) ) { $schema_types = $schema['type']; } if ( is_string( $schema['type'] ) ) { $schema_types[] = $schema['type']; } if ( in_array( 'object', $schema_types ) && isset( $schema['properties'] ) && is_array( $value ) ) { foreach ( $schema['properties'] as $key => $prop_schema ) { $value[ $key ] = $this->apply_schema_defaults( $value[ $key ] ?? null, $prop_schema ); } } if ( 'array' === $schema['type'] && isset( $schema['items'] ) && is_array( $value ) ) { foreach ( $value as $i => $item ) { $value[ $i ] = $this->apply_schema_defaults( $item, $schema['items'] ); } } return $value; } /** * table_slash() * * Add slashes to a string or strings in an array. * * This should be used instead of wp_slash() because wp_slash() convertes all * array values to strings which affects also the table object values of * type number converting to string. */ function table_slash( $value ) { if ( is_array( $value ) ) { foreach ( $value as $k => $v ) { if ( is_array( $v ) OR is_object( $v ) ) { $value[ $k ] = $this->table_slash( $v ); } else if( is_string( $v ) ) { $value[ $k ] = addslashes( $v ); } else { $value[ $k ] = $v; } } } else { if ( null !== $value ) { $value = addslashes( $value ); } } return $value; } } }