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(); ?>
data-form-id="id ); ?>" data-pages="" data-save-resume="" role="form" aria-label="title ); ?>"> title ) : ?>

title ); ?>

$page_fields ) : ?>
> render_field( $pair[1], $pair[0] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
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 = '
'; $out .= '
'; $out .= sprintf( '
', esc_attr( $name ), esc_attr( $sub ), esc_attr( $req_attr ), esc_attr__( 'Remove', 'phemightforms' ) ); $out .= '
'; $out .= ''; $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 = '
'; foreach ( $choices as $c_index => $choice ) { $parts = array_pad( explode( '|', $choice, 2 ), 2, '' ); $label = trim( $parts[0] ); $img = trim( $parts[1] ); $choice_id = $dom_id . '-' . $c_index; $out .= ''; } $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 ) ); } } }