array(
'label' => __( 'Short text', 'nonprofit-forms' ),
'input' => 'text',
),
'email' => array(
'label' => __( 'Email', 'nonprofit-forms' ),
'input' => 'email',
'sanitize' => 'sanitize_email',
'validate' => array( __CLASS__, 'validate_email' ),
),
'tel' => array(
'label' => __( 'Phone', 'nonprofit-forms' ),
'input' => 'tel',
'validate' => array( __CLASS__, 'validate_tel' ),
),
'number' => array(
'label' => __( 'Number', 'nonprofit-forms' ),
'input' => 'number',
'validate' => array( __CLASS__, 'validate_number' ),
),
'mobile_in' => array(
'label' => __( 'Indian mobile', 'nonprofit-forms' ),
'input' => 'tel',
'validate' => array( __CLASS__, 'validate_mobile_in' ),
),
'pin' => array(
'label' => __( 'PIN code', 'nonprofit-forms' ),
'input' => 'text',
'validate' => array( __CLASS__, 'validate_pin' ),
),
'state' => array(
'label' => __( 'State / UT', 'nonprofit-forms' ),
'render' => array( __CLASS__, 'render_state' ),
'validate' => array( __CLASS__, 'validate_state' ),
),
'address_in' => array(
'label' => __( 'Address: State, District, Town, PIN', 'nonprofit-forms' ),
'render' => array( __CLASS__, 'render_address_in' ),
'validate' => array( __CLASS__, 'validate_address_in' ),
),
'date' => array(
'label' => __( 'Date', 'nonprofit-forms' ),
'input' => 'date',
'validate' => array( __CLASS__, 'validate_date' ),
),
'textarea' => array(
'label' => __( 'Long text', 'nonprofit-forms' ),
'render' => array( __CLASS__, 'render_textarea' ),
'sanitize' => 'sanitize_textarea_field',
),
'select' => array(
'label' => __( 'Dropdown', 'nonprofit-forms' ),
'choices' => true,
'render' => array( __CLASS__, 'render_select' ),
),
'radio' => array(
'label' => __( 'Single choice', 'nonprofit-forms' ),
'choices' => true,
'render' => array( __CLASS__, 'render_choices' ),
),
'checkboxes' => array(
'label' => __( 'Multiple choice', 'nonprofit-forms' ),
'choices' => true,
'multiple' => true,
'render' => array( __CLASS__, 'render_choices' ),
),
'consent' => array(
'label' => __( 'Consent checkbox', 'nonprofit-forms' ),
'render' => array( __CLASS__, 'render_consent' ),
'validate' => array( __CLASS__, 'validate_consent' ),
),
);
/**
* Filter the available field types.
*
* @param array $types Field type definitions keyed by slug.
*/
self::$types = apply_filters( 'npfrm_field_types', $types );
}
return self::$types;
}
/**
* Get one type definition, falling back to text.
*
* @param string $slug Type slug.
* @return array
*/
public static function get( $slug ) {
$types = self::types();
return isset( $types[ $slug ] ) ? $types[ $slug ] : $types['text'];
}
/**
* Parse a choices textarea into a list of labels.
*
* @param array $field Field definition.
* @return string[]
*/
public static function choices( $field ) {
$raw = isset( $field['options'] ) ? (string) $field['options'] : '';
$lines = array_filter( array_map( 'trim', preg_split( '/\r\n|\r|\n/', $raw ) ) );
return array_values( array_unique( $lines ) );
}
/**
* Sanitise a clean-from-the-form list of field definitions (admin side).
*
* @param mixed $fields Decoded JSON.
* @return array
*/
public static function sanitize_definitions( $fields ) {
$clean = array();
$used = array();
if ( ! is_array( $fields ) ) {
return $clean;
}
foreach ( $fields as $field ) {
if ( ! is_array( $field ) ) {
continue;
}
$type = isset( $field['type'] ) ? sanitize_key( $field['type'] ) : 'text';
if ( ! isset( self::types()[ $type ] ) ) {
$type = 'text';
}
$label = isset( $field['label'] ) ? sanitize_text_field( $field['label'] ) : '';
$id = isset( $field['id'] ) ? sanitize_key( $field['id'] ) : '';
if ( '' === $id || isset( $used[ $id ] ) ) {
$id = 'f' . substr( md5( $label . count( $clean ) . wp_rand() ), 0, 6 );
}
$used[ $id ] = true;
$clean[] = array(
'id' => $id,
'type' => $type,
'label' => $label,
'required' => ! empty( $field['required'] ),
'placeholder' => isset( $field['placeholder'] ) ? sanitize_text_field( $field['placeholder'] ) : '',
'options' => isset( $field['options'] ) ? sanitize_textarea_field( $field['options'] ) : '',
'show_if' => self::sanitize_show_if( isset( $field['show_if'] ) ? $field['show_if'] : null ),
);
}
// A rule may only point at an earlier field that exists.
$seen = array();
foreach ( $clean as $i => $field ) {
if ( ! empty( $field['show_if']['field'] ) && ! isset( $seen[ $field['show_if']['field'] ] ) ) {
$clean[ $i ]['show_if'] = array();
}
$seen[ $field['id'] ] = true;
}
return $clean;
}
/**
* Clean a conditional rule: show this field only if another field equals (or not) a value.
*
* @param mixed $rule Raw rule.
* @return array Empty array means always show.
*/
public static function sanitize_show_if( $rule ) {
if ( ! is_array( $rule ) || empty( $rule['field'] ) ) {
return array();
}
return array(
'field' => sanitize_key( $rule['field'] ),
'op' => ( isset( $rule['op'] ) && 'not_equals' === $rule['op'] ) ? 'not_equals' : 'equals',
'value' => isset( $rule['value'] ) ? sanitize_text_field( $rule['value'] ) : '',
);
}
/**
* Is a field visible given the other submitted values? Mirrors the browser logic.
*
* @param array $field Field definition.
* @param array $values Sanitised values keyed by field id.
* @return bool
*/
public static function is_visible( $field, array $values ) {
if ( empty( $field['show_if']['field'] ) ) {
return true;
}
$rule = $field['show_if'];
$actual = isset( $values[ $rule['field'] ] ) ? $values[ $rule['field'] ] : '';
$match = is_array( $actual ) ? in_array( $rule['value'], $actual, true ) : ( (string) $actual === (string) $rule['value'] );
return 'equals' === $rule['op'] ? $match : ! $match;
}
/**
* Input name for a field.
*
* @param array $field Field.
* @return string
*/
public static function name( $field ) {
return 'npfrm_f_' . $field['id'];
}
/**
* Render one field wrapper plus input.
*
* @param array $field Field.
* @param mixed $value Previously submitted value.
* @param int $form_id Form ID.
* @param string $error Error message for this field.
* @return string HTML (already escaped).
*/
public static function render( $field, $value, $form_id, $error = '' ) {
$type = self::get( $field['type'] );
$dom_id = 'npfrm-' . $form_id . '-' . $field['id'];
$label = $field['label'];
$req = ! empty( $field['required'] );
$show_lb = 'consent' !== $field['type'];
$input = isset( $type['render'] )
? call_user_func( $type['render'], $field, $value, $dom_id, $type )
: self::render_input( $field, $value, $dom_id, $type );
$cond = '';
if ( ! empty( $field['show_if']['field'] ) ) {
$cond = ' data-npfrm-if="' . esc_attr( 'npfrm_f_' . $field['show_if']['field'] ) . '" data-npfrm-op="' . esc_attr( $field['show_if']['op'] ) . '" data-npfrm-val="' . esc_attr( $field['show_if']['value'] ) . '"';
}
$html = '
';
if ( $show_lb && '' !== $label ) {
$group = in_array( $field['type'], array( 'radio', 'checkboxes' ), true );
$tag = $group ? 'span' : 'label';
$for = $group ? '' : ' for="' . esc_attr( $dom_id ) . '"';
$html .= '<' . $tag . ' id="' . esc_attr( $dom_id ) . '-lbl" class="npfrm-label"' . $for . '>' . esc_html( $label );
if ( $req ) {
$html .= ' *' . esc_html__( '(required)', 'nonprofit-forms' ) . '';
}
$html .= '' . $tag . '>';
}
$html .= $input;
if ( $error ) {
$html .= '' . esc_html( $error ) . '';
}
return $html . '
';
}
/**
* Common attributes.
*
* @param array $field Field.
* @param string $dom_id DOM id.
* @return string
*/
private static function attrs( $field, $dom_id ) {
$out = ' id="' . esc_attr( $dom_id ) . '" name="' . esc_attr( self::name( $field ) ) . '"';
if ( ! empty( $field['required'] ) ) {
$out .= ' required aria-required="true"';
}
return $out . ' aria-describedby="' . esc_attr( $dom_id ) . '-err"';
}
/**
* Render a plain input.
*
* @param array $field Field.
* @param mixed $value Value.
* @param string $dom_id DOM id.
* @param array $type Type definition.
* @return string
*/
public static function render_input( $field, $value, $dom_id, $type ) {
$input = isset( $type['input'] ) ? $type['input'] : 'text';
return '';
}
/**
* Render a textarea.
*
* @param array $field Field.
* @param mixed $value Value.
* @param string $dom_id DOM id.
* @return string
*/
public static function render_textarea( $field, $value, $dom_id ) {
return '';
}
/**
* Render a dropdown.
*
* @param array $field Field.
* @param mixed $value Value.
* @param string $dom_id DOM id.
* @return string
*/
public static function render_select( $field, $value, $dom_id ) {
$html = '';
}
/**
* Render radio buttons or checkboxes.
*
* @param array $field Field.
* @param mixed $value Value.
* @param string $dom_id DOM id.
* @param array $type Type definition.
* @return string
*/
public static function render_choices( $field, $value, $dom_id, $type ) {
$multiple = ! empty( $type['multiple'] );
$value = (array) $value;
$html = '';
foreach ( self::choices( $field ) as $i => $choice ) {
$html .= '';
}
return $html . '
';
}
/**
* Render the consent checkbox. The label is the field label.
*
* @param array $field Field.
* @param mixed $value Value.
* @param string $dom_id DOM id.
* @return string
*/
public static function render_consent( $field, $value, $dom_id ) {
$link = '';
if ( ! empty( $field['privacy_url'] ) ) {
$link = ' (' . esc_html__( 'Privacy Policy', 'nonprofit-forms' ) . ')';
}
return '';
}
/**
* Sanitise a raw submitted value.
*
* @param array $field Field.
* @param mixed $raw Raw value (already unslashed).
* @return string|array
*/
public static function sanitize( $field, $raw ) {
$type = self::get( $field['type'] );
if ( ! empty( $type['multiple'] ) ) {
$raw = is_array( $raw ) ? $raw : array();
return array_values( array_map( 'sanitize_text_field', $raw ) );
}
if ( is_array( $raw ) ) {
return '';
}
$cb = isset( $type['sanitize'] ) ? $type['sanitize'] : 'sanitize_text_field';
return call_user_func( $cb, (string) $raw );
}
/**
* Validate a sanitised value. Returns an error string or ''.
*
* @param array $field Field.
* @param string|array $value Sanitised value.
* @return string
*/
public static function validate( $field, $value ) {
$type = self::get( $field['type'] );
$empty = ( '' === $value || array() === $value );
if ( $empty ) {
return ! empty( $field['required'] ) ? __( 'This field is required.', 'nonprofit-forms' ) : '';
}
if ( strlen( is_array( $value ) ? implode( ',', $value ) : $value ) > 5000 ) {
return __( 'This answer is too long.', 'nonprofit-forms' );
}
if ( in_array( $field['type'], array( 'select', 'radio', 'checkboxes' ), true ) ) {
$allowed = self::choices( $field );
foreach ( (array) $value as $v ) {
if ( ! in_array( $v, $allowed, true ) ) {
return __( 'Please choose a valid option.', 'nonprofit-forms' );
}
}
}
if ( isset( $type['validate'] ) ) {
return (string) call_user_func( $type['validate'], $value, $field );
}
return '';
}
/**
* Validate email.
*
* @param string $value Value.
* @return string
*/
public static function validate_email( $value ) {
return is_email( $value ) ? '' : __( 'Enter a valid email address.', 'nonprofit-forms' );
}
/**
* Validate phone: digits, spaces, plus, dash, brackets; 7 to 15 digits.
*
* @param string $value Value.
* @return string
*/
public static function validate_tel( $value ) {
$digits = preg_replace( '/\D/', '', $value );
if ( preg_match( '/^[0-9+\-\s()]+$/', $value ) && strlen( $digits ) >= 7 && strlen( $digits ) <= 15 ) {
return '';
}
return __( 'Enter a valid phone number.', 'nonprofit-forms' );
}
/**
* Validate number.
*
* @param string $value Value.
* @return string
*/
public static function validate_number( $value ) {
return is_numeric( $value ) ? '' : __( 'Enter a number.', 'nonprofit-forms' );
}
/**
* Validate date (YYYY-MM-DD).
*
* @param string $value Value.
* @return string
*/
public static function validate_date( $value ) {
$d = DateTime::createFromFormat( 'Y-m-d', $value );
return ( $d && $d->format( 'Y-m-d' ) === $value ) ? '' : __( 'Enter a valid date.', 'nonprofit-forms' );
}
/**
* Validate consent.
*
* @param string $value Value.
* @return string
*/
public static function validate_consent( $value ) {
return '1' === $value ? '' : __( 'Please tick the box to continue.', 'nonprofit-forms' );
}
/**
* Indian mobile: 10 digits starting 6-9, optional +91 or 0.
*
* @param string $value Value.
* @return string
*/
public static function validate_mobile_in( $value ) {
$digits = preg_replace( '/\D/', '', $value );
$digits = preg_replace( '/^(91|0)(?=[6-9]\d{9}$)/', '', $digits );
return preg_match( '/^[6-9]\d{9}$/', $digits ) ? '' : __( 'Enter a 10-digit Indian mobile number.', 'nonprofit-forms' );
}
/**
* Indian PIN code: 6 digits, not starting with 0.
*
* @param string $value Value.
* @return string
*/
public static function validate_pin( $value ) {
return preg_match( '/^[1-9]\d{5}$/', $value ) ? '' : __( 'Enter a 6-digit PIN code.', 'nonprofit-forms' );
}
/**
* States and Union Territories.
*
* @return string[]
*/
public static function states() {
return array( 'Andaman and Nicobar Islands', 'Andhra Pradesh', 'Arunachal Pradesh', 'Assam', 'Bihar', 'Chandigarh', 'Chhattisgarh', 'Dadra and Nagar Haveli and Daman and Diu', 'Delhi', 'Goa', 'Gujarat', 'Haryana', 'Himachal Pradesh', 'Jammu and Kashmir', 'Jharkhand', 'Karnataka', 'Kerala', 'Ladakh', 'Lakshadweep', 'Madhya Pradesh', 'Maharashtra', 'Manipur', 'Meghalaya', 'Mizoram', 'Nagaland', 'Odisha', 'Puducherry', 'Punjab', 'Rajasthan', 'Sikkim', 'Tamil Nadu', 'Telangana', 'Tripura', 'Uttar Pradesh', 'Uttarakhand', 'West Bengal' );
}
/**
* Render the State/UT dropdown.
*
* @param array $field Field.
* @param mixed $value Value.
* @param string $dom_id DOM id.
* @return string
*/
public static function render_state( $field, $value, $dom_id ) {
$field['options'] = implode( "\n", self::states() );
return self::render_select( $field, $value, $dom_id );
}
/**
* Render the India address cascade: State, District, Town, PIN code.
* The four dropdowns are filled on the page from the bundled India Post
* directory; only the combined value is submitted in a hidden input.
*
* @param array $field Field definition.
* @param string $value Current value.
* @param string $dom_id Element ID.
* @return string
*/
public static function render_address_in( $field, $value, $dom_id ) {
$row = function ( $cls, $id, $label, $control ) {
return '' . $control . '';
};
$ph = esc_html__( 'Select', 'nonprofit-forms' );
$html = '';
$html .= $row( 'state', $dom_id . '-state', __( 'State / UT', 'nonprofit-forms' ), '' );
$html .= $row( 'district', $dom_id . '-district', __( 'District', 'nonprofit-forms' ), '' );
$html .= $row(
'town',
$dom_id . '-town',
__( 'Town / Area', 'nonprofit-forms' ),
''
. ''
);
$html .= $row( 'pin', $dom_id . '-pin', __( 'PIN code', 'nonprofit-forms' ), '' );
$html .= '';
$html .= '' . esc_html__( 'PIN data: public copy of the India Post directory (official source: data.gov.in)', 'nonprofit-forms' ) . '';
return $html . '';
}
/**
* Validate the combined address value against the bundled India Post directory.
*
* @param string $value Submitted value.
* @return string Error message or empty string.
*/
public static function validate_address_in( $value ) {
$parts = array_map( 'trim', explode( ' | ', $value ) );
if ( 4 !== count( $parts ) ) {
return __( 'Please choose State, District, Town and PIN code.', 'nonprofit-forms' );
}
list( $state, $district, $town, $pin ) = $parts;
$data = self::pincode_data();
if ( ! is_array( $data ) ) {
return '';
}
if ( ! isset( $data[ $state ] ) ) {
return __( 'Please choose a valid state or UT.', 'nonprofit-forms' );
}
if ( ! isset( $data[ $state ][ $district ] ) ) {
return __( 'Please choose a valid district.', 'nonprofit-forms' );
}
foreach ( $data[ $state ][ $district ] as $row ) {
if ( $row[0] === $town ) {
if ( in_array( $pin, $row[1], true ) ) {
return '';
}
return __( 'Enter the 6-digit PIN code shown for the chosen town.', 'nonprofit-forms' );
}
}
return __( 'Please choose a valid town.', 'nonprofit-forms' );
}
/**
* Load the bundled India Post PIN directory once per request.
*
* @return array|null
*/
private static function pincode_data() {
static $data = null;
if ( null === $data ) {
$file = NPFRM_DIR . 'data/pincodes-in.json';
$json = is_readable( $file ) ? file_get_contents( $file ) : false;
$data = $json ? json_decode( $json, true ) : false;
if ( ! is_array( $data ) ) {
$data = false;
}
}
return $data ? $data : null;
}
/**
* Validate State/UT.
*
* @param string $value Value.
* @return string
*/
public static function validate_state( $value ) {
return in_array( $value, self::states(), true ) ? '' : __( 'Please choose a valid state or UT.', 'nonprofit-forms' );
}
}