admin_url( 'admin-ajax.php' ),
'currency' => Payments::symbol(),
'i18n' => [
'next' => __( 'Next', 'phemightforms' ),
'previous' => __( 'Previous', 'phemightforms' ),
'step' => __( 'Step', 'phemightforms' ),
'of' => __( 'of', 'phemightforms' ),
'couponApplied' => __( 'Coupon applied:', 'phemightforms' ),
'couponInvalid' => __( 'That coupon code is not valid.', 'phemightforms' ),
'redirecting' => __( 'Redirecting to secure checkout...', 'phemightforms' ),
],
]
);
}
/**
* Handle the [phemightform id="123"] shortcode.
*
* @param array $atts Shortcode attributes.
* @return string
*/
public function shortcode( $atts ) {
$atts = shortcode_atts(
[
'id' => 0,
'title' => 'true',
],
$atts,
'phemightform'
);
$form = new Form( (int) $atts['id'] );
if ( ! $form->exists() ) {
return '';
}
wp_enqueue_style( 'phemightforms' );
wp_enqueue_script( 'phemightforms' );
Captcha::enqueue();
Analytics::track_view( $form->id );
$embed_mode = $form->setting( 'embed_mode', 'inline' );
$html = $this->render( $form, 'false' === $atts['title'] );
if ( 'popup' === $embed_mode || 'slide' === $embed_mode ) {
$btn_text = $form->setting( 'embed_button_text', __( 'Open Form', 'phemightforms' ) );
return sprintf(
'
',
esc_attr( $embed_mode ),
esc_html( $btn_text ),
$html
);
}
return $html;
}
/**
* Split fields into pages using pagebreak fields as separators.
*
* @param array $fields Fields.
* @return array Array of pages, each an array of [index, field] pairs.
*/
private function paginate( array $fields ) {
$pages = [];
$current = [];
foreach ( $fields as $index => $field ) {
$type = isset( $field['type'] ) ? $field['type'] : 'text';
if ( 'pagebreak' === $type ) {
$pages[] = $current;
$current = [];
continue;
}
$current[] = [ $index, $field ];
}
$pages[] = $current;
return $pages;
}
/**
* Produce the HTML for a form.
*
* @param Form $form Form to render.
* @param bool $hide_title Whether to hide the form title heading.
* @return string
*/
public function render( Form $form, $hide_title = false ) {
$fields = $form->get_fields();
$nonce = wp_create_nonce( 'phemightforms_submit_' . $form->id );
$pages = $this->paginate( $fields );
$multipage = count( $pages ) > 1;
$has_file = $this->has_file_field( $fields );
$enctype = $has_file ? ' enctype="multipart/form-data"' : '';
$accent = sanitize_hex_color( $form->setting( 'accent_color', '#b56d18' ) );
$btn_text = sanitize_hex_color( $form->setting( 'button_text_color', '#ffffff' ) );
$form_bg = sanitize_hex_color( $form->setting( 'form_bg_color', '#ffffff' ) );
$text_color = sanitize_hex_color( $form->setting( 'text_color', '#0f172a' ) );
$border_c = sanitize_hex_color( $form->setting( 'border_color', '#e2e8f0' ) );
$border_w = min( 8, absint( $form->setting( 'border_width', 1 ) ) );
$radius = absint( $form->setting( 'border_radius', 12 ) );
$fsize = absint( $form->setting( 'font_size', 16 ) );
$btn_style = sanitize_key( $form->setting( 'button_style', 'solid' ) );
$resume = (int) $form->setting( 'save_resume', 0 );
$style = sprintf(
' style="--phf-accent:%1$s;--phf-radius:%2$spx;--phf-font-size:%3$spx;--phf-btn-text:%4$s;--phf-form-bg:%5$s;--phf-text:%6$s;--phf-border:%7$s;--phf-border-w:%8$spx;"',
esc_attr( $accent ? $accent : '#b56d18' ),
max( 0, min( 32, $radius ) ),
max( 12, min( 22, $fsize ) ),
esc_attr( $btn_text ? $btn_text : '#ffffff' ),
esc_attr( $form_bg ? $form_bg : '#ffffff' ),
esc_attr( $text_color ? $text_color : '#0f172a' ),
esc_attr( $border_c ? $border_c : '#e2e8f0' ),
$border_w
);
ob_start();
?>
isset( $field['conditional']['action'] ) ? $field['conditional']['action'] : 'show',
'logic' => isset( $field['conditional']['logic'] ) ? $field['conditional']['logic'] : 'and',
'rules' => $field['conditional']['rules'],
]
);
return " data-conditional='" . esc_attr( $payload ) . "'";
}
/**
* Render a single field row.
*
* @param array $field Field configuration.
* @param int $index Position in the config array (used only as a fallback id).
* @return string
*/
private function render_field( array $field, $index ) {
$type = isset( $field['type'] ) ? $field['type'] : 'text';
$fid = isset( $field['id'] ) && $field['id'] ? (int) $field['id'] : ( $index + 1 );
// Display-only fields.
if ( 'section' === $type ) {
return sprintf(
'%3$s
%4$s',
$fid,
$this->conditional_attr( $field ),
esc_html( isset( $field['label'] ) ? $field['label'] : '' ),
! empty( $field['description'] ) ? '' . esc_html( $field['description'] ) . '
' : ''
);
}
if ( 'html' === $type ) {
$content = isset( $field['html_content'] ) ? $field['html_content'] : '';
return sprintf(
'%3$s
',
$fid,
$this->conditional_attr( $field ),
wp_kses_post( $content )
);
}
if ( 'hidden' === $type ) {
return sprintf(
'',
$fid,
esc_attr( isset( $field['default_value'] ) ? $field['default_value'] : '' )
);
}
$label = isset( $field['label'] ) ? $field['label'] : '';
$required = ! empty( $field['required'] );
$place = isset( $field['placeholder'] ) ? $field['placeholder'] : '';
$desc = isset( $field['description'] ) ? $field['description'] : '';
$name = 'field_' . $fid;
$dom_id = 'phf-' . $fid;
$choices = isset( $field['choices'] ) && is_array( $field['choices'] ) ? $field['choices'] : [];
$req_attr = $required ? 'required aria-required="true"' : '';
$req_mark = $required ? ' *' : '';
$registry = phemightforms()->fields;
$size = isset( $field['size'] ) && 'half' === $field['size'] ? ' phf-field-half' : '';
$label_pos = isset( $field['label_position'] ) ? $field['label_position'] : '';
ob_start();
printf(
'',
esc_attr( $type ),
$fid, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- integer.
$this->conditional_attr( $field ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped internally.
esc_attr( $size ),
esc_attr( $label_pos ? $label_pos : 'top' )
);
$hide_label = 'hidden' === $label_pos || 'payment_total' === $type;
if ( ! $hide_label && '' !== $label ) {
if ( in_array( $type, [ 'checkbox', 'radio', 'name', 'address', 'image_choice' ], true ) ) {
echo '' . esc_html( $label ) . wp_kses_post( $req_mark ) . '';
} else {
echo '';
}
}
if ( $registry->is_composite( $type ) ) {
echo $this->render_composite( $type, $name, $req_attr, $registry->composite_parts( $type ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} else {
echo $this->render_simple( $type, $name, $dom_id, $place, $req_attr, $choices, $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
if ( '' !== $desc ) {
echo '' . esc_html( $desc ) . '';
}
echo '
';
return ob_get_clean();
}
/**
* Render a composite field (name, address) as several labelled sub-inputs.
*
* @param string $type Field type.
* @param string $name Base input name.
* @param string $req_attr Required attribute string.
* @param array $parts Sub-field keys.
* @return string
*/
private function render_composite( $type, $name, $req_attr, array $parts ) {
$labels = [
'first' => __( 'First', 'phemightforms' ),
'last' => __( 'Last', 'phemightforms' ),
'line1' => __( 'Address Line 1', 'phemightforms' ),
'line2' => __( 'Address Line 2', 'phemightforms' ),
'city' => __( 'City', 'phemightforms' ),
'state' => __( 'State / Province', 'phemightforms' ),
'zip' => __( 'ZIP / Postal Code', 'phemightforms' ),
'country' => __( 'Country', 'phemightforms' ),
];
$out = '';
foreach ( $parts as $part ) {
$sub_label = isset( $labels[ $part ] ) ? $labels[ $part ] : ucfirst( $part );
// "line2" is optional even when the field is required.
$sub_req = ( 'line2' === $part ) ? '' : $req_attr;
$out .= '';
$out .= sprintf(
'',
esc_attr( $name . '_' . $part ),
esc_attr( $sub_label ),
esc_attr( $sub_req )
);
$out .= '';
}
$out .= '
';
return $out;
}
/**
* Render a simple (single-input) field.
*
* @param string $type Field type.
* @param string $name Input name.
* @param string $dom_id Input DOM id.
* @param string $place Placeholder.
* @param string $req_attr Required attribute string.
* @param array $choices Choice list.
* @param array $field Full field config (for type-specific extras).
* @return string
*/
private function render_simple( $type, $name, $dom_id, $place, $req_attr, array $choices, array $field = [] ) {
switch ( $type ) {
case 'consent':
$consent_text = isset( $field['consent_text'] ) && '' !== $field['consent_text']
? $field['consent_text']
: __( 'I consent to having this website store my submitted information.', 'phemightforms' );
return sprintf(
'',
esc_attr( $dom_id ),
esc_attr( $name ),
esc_attr( $req_attr ),
esc_html( $consent_text )
);
case 'signature':
return sprintf(
'' .
'' .
'' .
'' .
'
',
esc_attr__( 'Signature pad', 'phemightforms' ),
esc_html__( 'Clear', 'phemightforms' ),
esc_attr( $name ),
esc_attr( $req_attr )
);
case 'repeater':
$sub = isset( $field['sub_label'] ) ? $field['sub_label'] : $place;
$out = '';
return $out;
case 'textarea':
return sprintf(
'',
esc_attr( $dom_id ),
esc_attr( $name ),
esc_attr( $place ),
esc_attr( $req_attr )
);
case 'file':
return sprintf(
'',
esc_attr( $dom_id ),
esc_attr( $name ),
esc_attr( $req_attr )
);
case 'select':
$out = '';
return $out;
case 'radio':
case 'checkbox':
$input_type = 'radio' === $type ? 'radio' : 'checkbox';
$field_name = 'checkbox' === $type ? $name . '[]' : $name;
$out = '';
foreach ( $choices as $c_index => $choice ) {
$choice_id = $dom_id . '-' . $c_index;
$out .= sprintf(
'',
esc_attr( $input_type ),
esc_attr( $choice_id ),
esc_attr( $field_name ),
esc_attr( $choice ),
esc_html( $choice )
);
}
$out .= '
';
return $out;
case 'image_choice':
$out = '';
return $out;
case 'rating':
$max = isset( $field['rating_max'] ) ? absint( $field['rating_max'] ) : 5;
$max = max( 3, min( 10, $max ) );
$out = '';
for ( $i = 1; $i <= $max; $i++ ) {
$out .= sprintf(
'',
esc_attr( $name ),
$i,
esc_attr( $req_attr )
);
}
$out .= '
';
return $out;
case 'country':
$out = '';
return $out;
case 'payment_single':
$price = Payments::to_amount( isset( $field['price'] ) ? $field['price'] : 0 );
if ( ! empty( $field['user_price'] ) ) {
return sprintf(
'%1$s
',
esc_html( Payments::symbol() ),
esc_attr( $dom_id ),
esc_attr( $name ),
esc_attr( $price > 0 ? $price : '' ),
esc_attr( $req_attr )
);
}
return sprintf(
'%2$s
',
esc_attr( $price ),
esc_html( Payments::format( $price ) ),
esc_attr( $name ),
esc_attr( $price )
);
case 'payment_multiple':
$out = '';
foreach ( $choices as $c_index => $choice ) {
list( $item_label, $item_price ) = Payments::parse_choice( $choice );
$choice_id = $dom_id . '-' . $c_index;
$out .= sprintf(
'',
esc_attr( $choice_id ),
esc_attr( $name ),
esc_attr( $item_label ),
esc_attr( $item_price ),
esc_attr( $req_attr ),
esc_html( $item_label ),
esc_html( Payments::format( $item_price ) )
);
}
$out .= '
';
return $out;
case 'payment_select':
$out = '';
return $out;
case 'payment_quantity':
return sprintf(
'',
esc_attr( $dom_id ),
esc_attr( $name ),
esc_attr( $req_attr )
);
case 'payment_coupon':
return sprintf(
'
',
esc_attr( $dom_id ),
esc_attr( $name ),
esc_attr( $place ? $place : __( 'Enter coupon code', 'phemightforms' ) ),
esc_html__( 'Apply', 'phemightforms' )
);
case 'payment_total':
return sprintf(
'%1$s%2$s
',
esc_html__( 'Total', 'phemightforms' ),
esc_html( Payments::format( 0 ) )
);
default:
$input_type = in_array( $type, [ 'email', 'number', 'url', 'date', 'time' ], true ) ? $type : ( 'phone' === $type ? 'tel' : 'text' );
return sprintf(
'',
esc_attr( $input_type ),
esc_attr( $dom_id ),
esc_attr( $name ),
esc_attr( $place ),
esc_attr( $req_attr )
);
}
}
}