Skip to content

Commit

Permalink
Merge pull request #723 from ConvertKit/form-position-element
Browse files Browse the repository at this point in the history
Define Default Form Position: Add `After element` option
  • Loading branch information
n7studios authored Oct 15, 2024
2 parents e49c6ce + 9747149 commit a9cb842
Show file tree
Hide file tree
Showing 11 changed files with 691 additions and 40 deletions.
3 changes: 2 additions & 1 deletion admin/section/class-convertkit-admin-settings-broadcasts.php
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,8 @@ public function enable_callback( $args ) {
'on',
$this->settings->enabled(), // phpcs:ignore WordPress.Security.EscapeOutput
$args['label'], // phpcs:ignore WordPress.Security.EscapeOutput
$args['description'] // phpcs:ignore WordPress.Security.EscapeOutput
$args['description'], // phpcs:ignore WordPress.Security.EscapeOutput
array( 'convertkit-conditional-display' )
);

}
Expand Down
32 changes: 32 additions & 0 deletions admin/section/class-convertkit-settings-base.php
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,38 @@ public function get_text_field( $name, $value = '', $description = false, $css_c

}

/**
* Returns a number field.
*
* @since 2.6.1
*
* @param string $name Name.
* @param string $value Value.
* @param int $min `min` attribute value.
* @param int $max `max` attribute value.
* @param int $step `step` attribute value.
* @param bool|string|array $description Description (false|string|array).
* @param bool|array $css_classes CSS Classes (false|array).
* @return string HTML Field
*/
public function get_number_field( $name, $value = '', $min = 0, $max = 9999, $step = 1, $description = false, $css_classes = false ) {

$html = sprintf(
'<input type="number" class="%s" id="%s" name="%s[%s]" value="%s" min="%s" max="%s" step="%s" />',
( is_array( $css_classes ) ? implode( ' ', $css_classes ) : 'small-text' ),
$name,
$this->settings_key,
$name,
$value,
$min,
$max,
$step
);

return $html . $this->get_description( $description );

}

/**
* Returns a textarea field.
*
Expand Down
75 changes: 68 additions & 7 deletions admin/section/class-convertkit-settings-general.php
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,9 @@ public function enqueue_scripts( $section ) {
// Enqueue Select2 JS.
convertkit_select2_enqueue_scripts();

// Enqueue Preview Output JS.
// Enqueue JS.
wp_enqueue_script( 'convertkit-admin-preview-output', CONVERTKIT_PLUGIN_URL . 'resources/backend/js/preview-output.js', array( 'jquery' ), CONVERTKIT_PLUGIN_VERSION, true );
wp_enqueue_script( 'convertkit-admin-settings-conditional-display', CONVERTKIT_PLUGIN_URL . 'resources/backend/js/settings-conditional-display.js', array( 'jquery' ), CONVERTKIT_PLUGIN_VERSION, true );

}

Expand Down Expand Up @@ -307,7 +308,19 @@ public function register_fields() {
$this->settings_key,
$this->name,
array(
'label_for' => '_wp_convertkit_settings_' . $supported_post_type . '_form',
'label_for' => '_wp_convertkit_settings_' . $supported_post_type . '_form_position',
'post_type' => $supported_post_type,
'post_type_object' => $post_type,
)
);
add_settings_field(
$supported_post_type . '_form_position_element',
'',
array( $this, 'default_form_position_element_callback' ),
$this->settings_key,
$this->name,
array(
'label_for' => '_wp_convertkit_settings_' . $supported_post_type . '_form_position_element',
'post_type' => $supported_post_type,
'post_type_object' => $post_type,
)
Expand Down Expand Up @@ -461,7 +474,6 @@ public function maybe_initialize_and_refresh_resources() {

$tags = new ConvertKit_Resource_Tags( 'settings' );
$tags->refresh();

}

/**
Expand Down Expand Up @@ -544,19 +556,68 @@ public function default_form_position_callback( $args ) {
$args['post_type'] . '_form_position',
esc_attr( $this->settings->get_default_form_position( $args['post_type'] ) ),
array(
'before_content' => esc_html__( 'Before content', 'convertkit' ),
'after_content' => esc_html__( 'After content', 'convertkit' ),
'before_after_content' => esc_html__( 'Before and after content', 'convertkit' ),
'before_content' => sprintf(
/* translators: Post type singular name */
esc_attr__( 'Before %s content', 'convertkit' ),
esc_attr( $args['post_type_object']->labels->singular_name )
),
'after_content' => sprintf(
/* translators: Post type singular name */
esc_attr__( 'After %s content', 'convertkit' ),
esc_attr( $args['post_type_object']->labels->singular_name )
),
'before_after_content' => sprintf(
/* translators: Post type singular name */
esc_attr__( 'Before and after %s content', 'convertkit' ),
esc_attr( $args['post_type_object']->labels->singular_name )
),
'after_element' => esc_html__( 'After element', 'convertkit' ),
),
sprintf(
/* translators: Post Type name, plural */
esc_html__( 'Where forms should display relative to the %s content', 'convertkit' ),
esc_html( $args['post_type_object']->label )
esc_html( $args['post_type_object']->labels->singular_name )
),
array( 'convertkit-conditional-display' ),
array(
'data-conditional-value' => 'after_element',
'data-conditional-element' => esc_attr( $args['post_type'] ) . '_form_position_element_index',
)
);

}

/**
* Renders the input for the Default Form Position Index setting for the given Post Type.
*
* @since 2.6.1
*
* @param array $args Field arguments.
*/
public function default_form_position_element_callback( $args ) {

echo $this->get_number_field( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$args['post_type'] . '_form_position_element_index',
esc_attr( (string) $this->settings->get_default_form_position_element_index( $args['post_type'] ) ),
1,
999,
1,
false,
array( 'after_element' )
);

echo $this->get_select_field( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$args['post_type'] . '_form_position_element',
esc_attr( $this->settings->get_default_form_position_element( $args['post_type'] ) ),
array(
'p' => esc_html__( 'Paragraphs', 'convertkit' ),
),
esc_html__( 'The number of elements before outputting the form.', 'convertkit' ),
array( 'after_element' )
);

}

/**
* Renders the input for the Non-inline Form setting.
*
Expand Down
122 changes: 122 additions & 0 deletions includes/class-convertkit-output.php
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,22 @@ public function append_form_to_content( $content ) {
$content = $form . $content;
break;

case 'after_element':
$element = $this->settings->get_default_form_position_element( get_post_type( $post_id ) );
$index = $this->settings->get_default_form_position_element_index( get_post_type( $post_id ) );

// Check if DOMDocument is installed.
// It should be installed as mosts hosts include php-dom and php-xml modules.
// If not, fallback to using preg_match_all(), which is less reliable.
if ( ! class_exists( 'DOMDocument' ) ) {
$content = $this->inject_form_after_element_fallback( $content, $element, $index, $form );
break;
}

// Use DOMDocument.
$content = $this->inject_form_after_element( $content, $element, $index, $form );
break;

case 'after_content':
default:
// Default behaviour < 2.5.8 was to append the Form after the content.
Expand All @@ -362,6 +378,112 @@ public function append_form_to_content( $content ) {

}

/**
* Injects the form after the given element and index, using DOMDocument.
*
* @since 2.6.2
*
* @param string $content Page / Post Content.
* @param string $tag HTML tag to insert form after.
* @param int $index Number of $tag elements to find before inserting form.
* @param string $form Form HTML to inject.
* @return string
*/
private function inject_form_after_element( $content, $tag, $index, $form ) {

// Load Page / Post content into DOMDocument.
libxml_use_internal_errors( true );
$html = new DOMDocument();
$html->loadHTML( $content, LIBXML_HTML_NODEFDTD );

// Find the element to append the form to.
// item() is a zero based index.
$element_node = $html->getElementsByTagName( $tag )->item( $index - 1 );

// If the element could not be found, either the number of elements by tag name is less
// than the requested position the form be inserted in, or no element exists.
// Append the form to the content and return.
if ( is_null( $element_node ) ) {
return $content . $form;
}

// Create new element for the Form.
$form_node = new DOMDocument();
$form_node->loadHTML( $form, LIBXML_HTML_NODEFDTD );

// Append the form to the specific element.
$element_node->parentNode->insertBefore( $html->importNode( $form_node->documentElement, true ), $element_node->nextSibling ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase

// Fetch HTML string.
$content = $html->saveHTML();

// Remove some HTML tags that DOMDocument adds, returning the output.
// We do this instead of using LIBXML_HTML_NOIMPLIED in loadHTML(), because Legacy Forms are not always contained in
// a single root / outer element, which is required for LIBXML_HTML_NOIMPLIED to correctly work.
$content = str_replace( '<html>', '', $content );
$content = str_replace( '</html>', '', $content );
$content = str_replace( '<head>', '', $content );
$content = str_replace( '</head>', '', $content );
$content = str_replace( '<body>', '', $content );
$content = str_replace( '</body>', '', $content );

return $content;

}

/**
* Injects the form after the given element and index, using preg_match_all().
* This is less reliable than DOMDocument, and is called if DOMDocument is
* not installed on the server.
*
* @since 2.6.2
*
* @param string $content Page / Post Content.
* @param string $tag HTML tag to insert form after.
* @param int $index Number of $tag elements to find before inserting form.
* @param string $form Form HTML to inject.
* @return string
*/
private function inject_form_after_element_fallback( $content, $tag, $index, $form ) {

// Calculate tag length.
$tag_length = ( strlen( $tag ) + 3 );

// Find all closing elements.
preg_match_all( '/<\/' . $tag . '>/', $content, $matches );

// If no elements exist, just append the form.
if ( count( $matches[0] ) === 0 ) {
$content = $content . $form;
return $content;
}

// If the number of elements is less than the index, we don't have enough elements to add the form to.
// Just add the form after the content.
if ( count( $matches[0] ) <= $index ) {
$content = $content . $form;
return $content;
}

// Iterate through the content to find the element at the configured index e.g. find the 4th closing paragraph.
$offset = 0;
foreach ( $matches[0] as $element_index => $element ) {
$position = strpos( $content, $element, $offset );
if ( ( $element_index + 1 ) === $index ) {
return substr( $content, 0, $position + 4 ) . $form . substr( $content, $position + 4 );
}

// Increment offset.
$offset = $position + 1;
}

// If here, something went wrong.
// Just add the form after the content.
$content = $content . $form;
return $content;

}

/**
* Registers the ConvertKit Form block to before or after the Query Loop block, when viewing a Category archive.
*
Expand Down
44 changes: 42 additions & 2 deletions includes/class-convertkit-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,44 @@ public function get_default_form_position( $post_type ) {

}

/**
* Returns the Default Form Position Element Plugin setting.
*
* @since 2.6.1
*
* @param string $post_type Post Type.
* @return string Element to insert form after
*/
public function get_default_form_position_element( $post_type ) {

// Return after_content if this Post Type's position doesn't exist as a setting.
if ( ! array_key_exists( $post_type . '_form_position_element', $this->settings ) ) {
return 'p';
}

return $this->settings[ $post_type . '_form_position_element' ];

}

/**
* Returns the Default Form Position Index Plugin setting.
*
* @since 2.6.1
*
* @param string $post_type Post Type.
* @return int Number of elements before inserting form
*/
public function get_default_form_position_element_index( $post_type ) {

// Return 1 if this Post Type's position index doesn't exist as a setting.
if ( ! array_key_exists( $post_type . '_form_position_element_index', $this->settings ) ) {
return 1;
}

return (int) $this->settings[ $post_type . '_form_position_element_index' ];

}

/**
* Returns the Global non-inline Form Plugin setting.
*
Expand Down Expand Up @@ -406,8 +444,10 @@ public function get_defaults() {

// Add Post Type Default Forms.
foreach ( convertkit_get_supported_post_types() as $post_type ) {
$defaults[ $post_type . '_form' ] = 0; // -1, 0 or Form ID.
$defaults[ $post_type . '_form_position' ] = 'after_content'; // before_content,after_content.
$defaults[ $post_type . '_form' ] = 0; // -1, 0 or Form ID.
$defaults[ $post_type . '_form_position' ] = 'after_content'; // before_content,after_content,before_after_content,element.
$defaults[ $post_type . '_form_position_element' ] = 'p';
$defaults[ $post_type . '_form_position_element_index' ] = 1;
}

/**
Expand Down
Loading

0 comments on commit a9cb842

Please sign in to comment.