-
Notifications
You must be signed in to change notification settings - Fork 826
Forms: Display success info after form submission without reload #44204
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8e7b535
13c1bf1
5d6cb8a
a5a6628
0eee8f5
c0bf111
8b09522
9291356
2f30fd6
e0d0078
dd876ea
f378e86
54f23ef
b5daaf5
c6b5f66
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
Significance: patch | ||
Type: added | ||
|
||
Forms: Display success info after form submission without reload |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,7 @@ | |
use PHPMailer\PHPMailer\PHPMailer; | ||
use WP_Block; | ||
use WP_Error; | ||
use WP_Post; | ||
|
||
/** | ||
* Class for the contact-form shortcode. | ||
|
@@ -78,6 +79,20 @@ class Contact_Form extends Contact_Form_Shortcode { | |
*/ | ||
public static $allowed_html_tags_for_submit_button = array( 'br' => array() ); | ||
|
||
/** | ||
* Whether to enable response without reloading the page. | ||
* | ||
* @var bool | ||
*/ | ||
public $is_response_without_reload_enabled = false; | ||
|
||
/** | ||
* The current post object for this form. | ||
* | ||
* @var WP_Post|null | ||
*/ | ||
public $current_post; | ||
|
||
/** | ||
* Construction function. | ||
* | ||
|
@@ -87,6 +102,18 @@ class Contact_Form extends Contact_Form_Shortcode { | |
public function __construct( $attributes, $content = null ) { | ||
global $post, $page; | ||
|
||
// AJAX requests don't have a post object, so we need to get the post object from the $_POST['contact-form-id'] | ||
$this->current_post = $post; | ||
|
||
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verification happens in process_form_submission() for logged-in users | ||
if ( ! $this->current_post && isset( $_POST['contact-form-id'] ) ) { | ||
$contact_form_id = sanitize_text_field( wp_unslash( $_POST['contact-form-id'] ) ); | ||
$this->current_post = get_post( $contact_form_id ); | ||
} | ||
// phpcs:enable | ||
|
||
$this->is_response_without_reload_enabled = apply_filters( 'jetpack_forms_enable_ajax_submission', false ); | ||
|
||
// Set up the default subject and recipient for this form. | ||
$default_to = ''; | ||
$default_subject = '[' . get_option( 'blogname' ) . ']'; | ||
|
@@ -95,12 +122,12 @@ public function __construct( $attributes, $content = null ) { | |
$attributes = array(); | ||
} | ||
|
||
if ( $post ) { | ||
if ( $this->current_post ) { | ||
$default_subject = sprintf( | ||
// translators: the blog name and post title. | ||
_x( '%1$s %2$s', '%1$s = blog name, %2$s = post title', 'jetpack-forms' ), | ||
$default_subject, | ||
Contact_Form_Plugin::strip_tags( $post->post_title ) | ||
Contact_Form_Plugin::strip_tags( $this->current_post->post_title ) | ||
); | ||
} | ||
|
||
|
@@ -115,17 +142,18 @@ public function __construct( $attributes, $content = null ) { | |
} elseif ( ! empty( $attributes['block_template_part'] ) && $attributes['block_template_part'] ) { | ||
$default_to .= get_option( 'admin_email' ); | ||
$attributes['id'] = 'block-template-part-' . $attributes['block_template_part']; | ||
} elseif ( $post ) { | ||
$attributes['id'] = $post->ID; | ||
$post_author = get_userdata( $post->post_author ); | ||
} elseif ( $this->current_post ) { | ||
$attributes['id'] = $this->current_post->ID; | ||
$post_author = get_userdata( $this->current_post->post_author ); | ||
if ( is_a( $post_author, '\WP_User' ) ) { | ||
$default_to .= $post_author->user_email; | ||
} else { | ||
$default_to .= get_option( 'admin_email' ); | ||
} | ||
} | ||
|
||
if ( ! empty( self::$forms ) ) { | ||
// When using admin-ajax.php, we don't need to add a page number to the id | ||
if ( ! empty( self::$forms ) && ! $this->is_response_without_reload_enabled ) { | ||
// Ensure 'id' exists in $attributes before trying to modify it | ||
if ( ! isset( $attributes['id'] ) ) { | ||
$attributes['id'] = ''; | ||
|
@@ -328,8 +356,56 @@ public static function parse( $attributes, $content, $context = array() ) { | |
$container_classes[] = self::get_block_alignment_class( $attributes ); | ||
$container_classes_string = implode( ' ', $container_classes ); | ||
|
||
$max_steps = 0; | ||
if ( preg_match_all( '/data-wp-context=[\'"]?{"step":(\d+)}[\'"]?/', $content, $matches ) ) { | ||
if ( ! empty( $matches[1] ) ) { | ||
$max_steps = max( array_map( 'intval', $matches[1] ) ); | ||
} | ||
} | ||
|
||
$is_multistep = $max_steps > 0; | ||
$element_id = 'jp-form-' . esc_attr( $form->hash ); | ||
|
||
$default_context = array( | ||
'formId' => $id, | ||
'formHash' => $form->hash, | ||
'showErrors' => false, // We toggle this to true when we want to show the user errors right away. | ||
'errors' => array(), // This should be a associative array. | ||
'fields' => array(), | ||
'isMultiStep' => $is_multistep, // Whether the form is a multistep form. | ||
'isResponseWithoutReloadEnabled' => $form->is_response_without_reload_enabled, | ||
'submissionData' => null, | ||
'submissionError' => null, | ||
'elementId' => $element_id, | ||
); | ||
|
||
if ( $is_multistep ) { | ||
$multistep_context = array( | ||
'currentStep' => isset( $_GET[ $id . '-step' ] ) ? absint( $_GET[ $id . '-step' ] ) : 1, // phpcs:ignore WordPress.Security.NonceVerification.Recommended | ||
'maxSteps' => $max_steps, | ||
'direction' => 'forward', // Default direction for animations | ||
'transition' => $form->get_attribute( 'stepTransition' ) ? $form->get_attribute( 'stepTransition' ) : 'fade-slide', // Transition style for step animations | ||
); | ||
|
||
if ( ! is_array( $context ) ) { | ||
$context = array(); | ||
} | ||
$context = array_merge( $context, $multistep_context ); | ||
} | ||
|
||
$context = is_array( $context ) ? array_merge( $default_context, $context ) : $default_context; | ||
|
||
$r = ''; | ||
$r .= "<div data-test='contact-form' id='contact-form-$id' class='{$container_classes_string}'>\n"; | ||
$r .= "<div data-test='contact-form' | ||
id='contact-form-$id' | ||
class='{$container_classes_string}' | ||
data-wp-interactive='jetpack/form' " . wp_interactivity_data_wp_context( $context ) . " | ||
data-wp-watch--scroll-to-wrapper=\"callbacks.scrollToWrapper\" | ||
>\n"; | ||
|
||
if ( $form->is_response_without_reload_enabled ) { | ||
$r .= self::render_ajax_success_wrapper( $form ); | ||
} | ||
|
||
if ( is_wp_error( $form->errors ) && $form->errors->get_error_codes() ) { | ||
// There are errors. Display them | ||
|
@@ -418,49 +494,16 @@ public static function parse( $attributes, $content, $context = array() ) { | |
$form_classes .= ' wp-block-jetpack-contact-form'; | ||
} | ||
|
||
$max_steps = 0; | ||
if ( preg_match_all( '/data-wp-context=[\'"]?{"step":(\d+)}[\'"]?/', $content, $matches ) ) { | ||
if ( ! empty( $matches[1] ) ) { | ||
$max_steps = max( array_map( 'intval', $matches[1] ) ); | ||
} | ||
} | ||
|
||
$is_multistep = $max_steps > 0; | ||
|
||
$default_context = array( | ||
'formId' => $id, | ||
'formHash' => $form->hash, | ||
'showErrors' => false, // We toggle this to true when we want to show the user errors right away. | ||
'errors' => array(), // This should be a associative array. | ||
'fields' => array(), | ||
'isMultiStep' => $is_multistep, // Whether the form is a multistep form. | ||
'isAjaxSubmissionEnabled' => apply_filters( 'jetpack_forms_enable_ajax_submission', false ), | ||
); | ||
|
||
if ( $is_multistep ) { | ||
$multistep_context = array( | ||
'currentStep' => isset( $_GET[ $id . '-step' ] ) ? absint( $_GET[ $id . '-step' ] ) : 1, | ||
'maxSteps' => $max_steps, | ||
'direction' => 'forward', // Default direction for animations | ||
'transition' => $form->get_attribute( 'stepTransition' ) ? $form->get_attribute( 'stepTransition' ) : 'fade-slide', // Transition style for step animations | ||
); | ||
|
||
if ( ! is_array( $context ) ) { | ||
$context = array(); | ||
} | ||
$context = array_merge( $context, $multistep_context ); | ||
} | ||
|
||
$context = is_array( $context ) ? array_merge( $default_context, $context ) : $default_context; | ||
|
||
$r .= "<form action='" . esc_url( $url ) . "' | ||
id='jp-form-" . esc_attr( $form->hash ) . "' | ||
id='" . $element_id . "' | ||
method='post' | ||
class='" . esc_attr( $form_classes ) . "' $form_aria_label | ||
data-wp-interactive=\"jetpack/form\" " . wp_interactivity_data_wp_context( $context ) . " | ||
data-wp-on--submit=\"actions.onFormSubmit\" | ||
data-wp-on--reset=\"actions.onFormReset\" | ||
data-wp-class--is-submitted=\"state.hasSubmitted\" | ||
data-wp-class--is-first-step=\"state.isFirstStep\" | ||
data-wp-class--is-last-step=\"state.isLastStep\" | ||
data-wp-class--is-ajax-form=\"context.isResponseWithoutReloadEnabled\" | ||
novalidate >\n"; | ||
|
||
if ( $is_multistep ) { // This makes the "enter" key work in multi-step forms as expected. | ||
|
@@ -577,6 +620,49 @@ private static function render_error_wrapper() { | |
return $html; | ||
} | ||
|
||
/** | ||
* Renders the success wrapper after a form is submitted without reloading the page. | ||
* | ||
* @param Contact_Form $form - the contact form. | ||
* | ||
* @return string HTML string for the success wrapper. | ||
*/ | ||
private static function render_ajax_success_wrapper( $form ) { | ||
$html = '<div class="contact-form-submission contact-form-ajax-submission" data-wp-class--is-submitted="state.hasSubmitted">'; | ||
$html .= '<p class="go-back-message"> <a class="link" href="#" data-wp-on--click="actions.goBack">' . esc_html__( 'Go back', 'jetpack-forms' ) . '</a> </p>'; | ||
$html .= | ||
'<h4 id="contact-form-success-header">' . esc_html( $form->get_attribute( 'customThankyouHeading' ) ) . | ||
"</h4>\n\n"; | ||
|
||
if ( 'message' === $form->get_attribute( 'customThankyou' ) ) { | ||
$raw_message = wpautop( $form->get_attribute( 'customThankyouMessage' ) ); | ||
// Add more allowed HTML elements for file download links | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why be restrictive here at all instead of just using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is the current implementation on the post-submission screen. The idea is to match it as well as possible, so I copied it over. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense then 👍 |
||
$allowed_html = array( | ||
'br' => array(), | ||
'blockquote' => array( 'class' => array() ), | ||
'p' => array(), | ||
'div' => array( | ||
'class' => array(), | ||
'style' => array(), | ||
), | ||
'span' => array( | ||
'class' => array(), | ||
'style' => array(), | ||
), | ||
); | ||
|
||
$html .= wp_kses( $raw_message, $allowed_html ); | ||
} else { | ||
$html .= '<template data-wp-each--submission="state.getSubmissionData"> | ||
<div class="field-name" data-wp-text="context.submission.label" data-wp-bind--hidden="!context.submission.label"></div> | ||
<div class="field-value" data-wp-text="context.submission.value"></div> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as https://github.com/Automattic/jetpack/pull/44204/files#r2189419246 — any reason not to prefix class with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can prefix them, yes. I just need to make sure the prefixed classes also work on the current screen. Will add it in this PR. |
||
</template>'; | ||
} | ||
|
||
$html .= '</div>'; | ||
return $html; | ||
} | ||
|
||
/** | ||
* Returns a success message to be returned if the form is sent via AJAX. | ||
* | ||
|
@@ -1240,8 +1326,6 @@ public function get_field_ids() { | |
* Stores feedback. Sends email. | ||
*/ | ||
public function process_submission() { | ||
global $post; | ||
|
||
$plugin = Contact_Form_Plugin::init(); | ||
|
||
$id = $this->get_attribute( 'id' ); | ||
|
@@ -1295,7 +1379,7 @@ public function process_submission() { | |
if ( isset( $_POST['contact-form-id'] ) && 'block-template-part-' . $block_template_part !== $_POST['contact-form-id'] ) { // phpcs:Ignore WordPress.Security.NonceVerification.Missing -- check done by caller process_form_submission() | ||
return false; | ||
} | ||
} elseif ( isset( $_POST['contact-form-id'] ) && ( empty( $post ) || $post->ID !== (int) $_POST['contact-form-id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- check done by caller process_form_submission() | ||
} elseif ( isset( $_POST['contact-form-id'] ) && ( empty( $this->current_post ) || $this->current_post->ID !== (int) sanitize_text_field( wp_unslash( $_POST['contact-form-id'] ) ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- check done by caller process_form_submission() | ||
return false; | ||
} | ||
|
||
|
@@ -1575,9 +1659,29 @@ public function process_submission() { | |
$feedback_title = "{$comment_author} - {$feedback_time}"; | ||
$feedback_id = md5( $feedback_title ); | ||
|
||
$entry_title = ''; | ||
$entry_permalink = ''; | ||
|
||
if ( $this->current_post ) { | ||
$entry_title = $this->current_post->post_title; | ||
$entry_permalink = esc_url( self::get_permalink( $this->current_post->ID ) ); | ||
} elseif ( $widget ) { | ||
$entry_title = __( 'Sidebar Widget', 'jetpack-forms' ); | ||
$entry_permalink = esc_url( home_url( '/' ) ); | ||
} elseif ( $block_template ) { | ||
$entry_title = __( 'Block Template', 'jetpack-forms' ); | ||
$entry_permalink = esc_url( home_url( '/' ) ); | ||
} elseif ( $block_template_part ) { | ||
$entry_title = __( 'Block Template Part', 'jetpack-forms' ); | ||
$entry_permalink = esc_url( home_url( '/' ) ); | ||
} else { | ||
$entry_title = the_title_attribute( 'echo=0' ); | ||
$entry_permalink = esc_url( self::get_permalink( get_the_ID() ) ); | ||
} | ||
|
||
$entry_values = array( | ||
'entry_title' => the_title_attribute( 'echo=0' ), | ||
'entry_permalink' => esc_url( self::get_permalink( get_the_ID() ) ), | ||
'entry_title' => $entry_title, | ||
'entry_permalink' => $entry_permalink, | ||
'feedback_id' => $feedback_id, | ||
); | ||
|
||
|
@@ -1596,7 +1700,7 @@ public function process_submission() { | |
if ( $block_template || $block_template_part || $widget ) { | ||
$url = home_url( '/' ); | ||
} else { | ||
$url = self::get_permalink( $post->ID ); | ||
$url = self::get_permalink( $this->current_post ? $this->current_post->ID : 0 ); | ||
} | ||
|
||
// translators: the time of the form submission. | ||
|
@@ -1661,7 +1765,7 @@ public function process_submission() { | |
'post_date' => addslashes( $feedback_time ), | ||
'post_type' => 'feedback', | ||
'post_status' => addslashes( $feedback_status ), | ||
'post_parent' => $post ? (int) $post->ID : 0, | ||
'post_parent' => $this->current_post ? (int) $this->current_post->ID : 0, | ||
'post_title' => addslashes( wp_kses( $feedback_title, array() ) ), | ||
// phpcs:ignore WordPress.NamingConventions.ValidVariableName.InterpolatedVariableNotSnakeCase, WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.PHP.DevelopmentFunctions.error_log_print_r | ||
'post_content' => addslashes( wp_kses( "$comment_content\n<!--more-->\nAUTHOR: {$comment_author}\nAUTHOR EMAIL: {$comment_author_email}\nAUTHOR URL: {$comment_author_url}\nSUBJECT: {$subject}\n{$comment_ip_text}JSON_DATA\n" . @wp_json_encode( $all_values, true ), array() ) ), // so that search will pick up this data | ||
|
@@ -1884,16 +1988,14 @@ public function process_submission() { | |
do_action( 'grunion_after_message_sent', $post_id, $to, $subject, $message, $headers, $all_values, $extra_values ); | ||
|
||
// If the request accepts JSON, return a JSON response instead of redirecting | ||
$is_ajax_submission_enabled = apply_filters( 'jetpack_forms_enable_ajax_submission', false ); | ||
$accepts_json = isset( $_SERVER['HTTP_ACCEPT'] ) && false !== strpos( strtolower( sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT'] ) ) ), 'application/json' ); | ||
$accepts_json = isset( $_SERVER['HTTP_ACCEPT'] ) && false !== strpos( strtolower( sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT'] ) ) ), 'application/json' ); | ||
|
||
if ( $is_ajax_submission_enabled && $accepts_json ) { | ||
if ( $this->is_response_without_reload_enabled && $accepts_json ) { | ||
header( 'Content-Type: application/json' ); | ||
|
||
echo wp_json_encode( | ||
array( | ||
'success' => true, | ||
'message' => __( 'Your message has been sent', 'jetpack-forms' ), | ||
'data' => self::get_json_data( $post_id, $this ), | ||
) | ||
); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are the CSS classes these for backwards compatibility, or would it make sense to add
jetpack-
prefix to scope them better?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those are for backwards compatibility, yes. That is almost a copy of the current post-submission screen.