LLMS_Forms_Dynamic_Fields
Manage dynamically generated fields added to the form outside of the block editor
Contents
Source Source
File: includes/forms/class-llms-forms-dynamic-fields.php
class LLMS_Forms_Dynamic_Fields { /** * Constructor * * @since 5.0.0 * @since 5.1.0 Added logic to make sure forms have all the required fields. * * @return void */ public function __construct() { add_filter( 'llms_get_form_blocks', array( $this, 'add_password_strength_meter' ), 10, 2 ); add_filter( 'llms_get_form_blocks', array( $this, 'maybe_add_required_block_fields' ), 10, 3 ); add_filter( 'llms_get_form_blocks', array( $this, 'modify_account_form' ), 15, 2 ); } /** * Creates a new HTML block with the given settings and inserts it into an existing blocks array at the specified location * * @since 5.0.0 * * @param array[] $blocks Array of WP_Block arrays. * @param array $block_settings Block attributes used to generate a new custom HTML field block. * @param integer $index Desired index of the new block. * * @return array[] */ private function add_block( $blocks, $block_settings, $index ) { // Make the new block. $add_block = parse_blocks( LLMS_Forms::instance()->get_custom_field_block_markup( $block_settings ) ); // Add it into the form after the specified index. array_splice( $blocks, $index + 1, 0, $add_block ); return $blocks; } /** * Adds a password strength meter to a block list * * This function will programmatically add an html block containing the necessary * markup for the password strength meter to function. * * This will locate the user password block and output the meter immediately after * the block. If the password block is within a group it'll output it after the * group block. * * @since 5.0.0 * @since 5.0.1 Add `aria-live=polite` to ensure password strength is announced for screen readers. * * @param array[] $blocks WP_Block list. * @return array[] */ public function add_password_strength_meter( $blocks, $location ) { $password = $this->find_block( 'password', $blocks ); // No password field in the form. if ( ! $password ) { return $blocks; } list( $index, $block ) = $password; // Meter not enabled. if ( empty( $block['attrs']['meter'] ) || ! llms_parse_bool( $block['attrs']['meter'] ) ) { return $blocks; } $meter_settings = array( 'type' => 'html', 'id' => 'llms-password-strength-meter', 'classes' => 'llms-password-strength-meter', 'description' => ! empty( $block['attrs']['meter_description'] ) ? $block['attrs']['meter_description'] : '', 'min_length' => ! empty( $block['attrs']['html_attrs']['minlength'] ) ? $block['attrs']['html_attrs']['minlength'] : '', 'min_strength' => ! empty( $block['attrs']['min_strength'] ) ? $block['attrs']['min_strength'] : '', 'llms_visibility' => ! empty( $block['attrs']['llms_visibility'] ) ? $block['attrs']['llms_visibility'] : '', 'attributes' => array( 'aria-live' => 'polite', ), ); if ( 'account' === $location ) { $meter_settings['wrapper_classes'] = 'llms-visually-hidden-field'; } /** * Filters the settings used to create the dynamic password strength meter block * * @since 5.0.0 * * @param array $meter_settings Array or block attributes/settings. */ $meter_settings = apply_filters( 'llms_password_strength_meter_field_settings', $meter_settings ); return $this->add_block( $blocks, $meter_settings, $index ); } /** * Finds a block with the specified ID within a list of blocks * * There's a gotcha with this function... if a user password field is placed within a wp core columns block * the password strength meter will be added outside the column the password is contained within. * * @since 5.0.0 * * @param string $id The ID of the field to find. * @param array[] $blocks WP_Block list. * @param integer $parent_index Top level index of the parent block. Used to hold a reference to the current index within the toplevel * blocks of the form when looking into the innerBlocks of a block. * @return boolean|array Returns `false` when the block cannot be found in the given list, otherwise returns a numeric array * where item `0` is the index of the block within the list (the index of the items parent if it's in a * group) and item `1` is the block array. */ private function find_block( $id, $blocks, $parent_index = null ) { foreach ( $blocks as $index => $block ) { if ( ! empty( $block['attrs']['id'] ) && $id === $block['attrs']['id'] ) { return array( is_null( $parent_index ) ? $index : $parent_index, $block ); } if ( $block['innerBlocks'] ) { $inner = $this->find_block( $id, $block['innerBlocks'], is_null( $parent_index ) ? $index : $parent_index ); if ( false !== $inner ) { return $inner; } } } return false; } /** * Retrieve the fields required for a given location based on user state * * @since 5.1.0 * * @param string $location The request form location ID. * @param array $args Additional arguments passed to the short-circuit filter. * @return array[] Array of field_id => block_name required or an empty array if no fields required. */ private function get_required_fields_for_location( $location, $args ) { $fields = array(); if ( ( ! is_user_logged_in() && in_array( $location, array( 'checkout', 'registration' ), true ) ) || ( is_user_logged_in() && 'account' === $location ) ) { $fields = array( // Field ID => block name. 'email_address' => 'email', 'password' => 'password', ); } /** * Filters the required block fields to add to the form * * @since 5.1.0 * * @param array[] $fields Array of field_id => block_name required. * @param string $location The request form location ID. * @param array $args Additional arguments passed to the short-circuit filter. */ return apply_filters( 'llms_forms_required_block_fields', $fields, $location, $args ); } /** * Retrieve the HTML for a field toggle button link * * @since 5.0.0 * * @param string $fields A comma-separated list of selectors for the controlled fields. * @param string $field_label Label for the original field. * @return string */ private function get_toggle_button_html( $fields, $field_label ) { // Translator: %s = user-selected label for the given field being toggled. $change_text = sprintf( esc_attr_x( 'Change %s', 'Toggle button for changing email or password', 'lifterlms' ), $field_label ); $cancel_text = esc_attr_x( 'Cancel', 'Cancel password or email address change button text', 'lifterlms' ); return '<a class="llms-toggle-fields" data-fields="' . $fields . '" data-change-text="' . $change_text . '" data-cancel-text="' . $cancel_text . '" href="#">' . $change_text . '</a>'; } /** * Modifies account form to improve the UX of editing the email address and password fields * * Adds a "Current Password" field used to verify the existing user password when changing passwords. * * Forces email & password fields to be required and makes them disabled and visually hidden on page load. * * Adds a toggle button for each set of fields, when the toggle is clicked the fields are revealed and enabled * so they can be used. Ensuring that the fields are only required when they're being explicitly changed. * * @since 5.0.0 * * @param array[] $blocks Array of parsed WP_Block arrays. * @param string $location The form location ID. * * @return array[] */ public function modify_account_form( $blocks, $location ) { // Only add toggles on the account edit form. if ( 'account' !== $location ) { return $blocks; } $blocks = $this->modify_toggle_blocks( $blocks ); foreach ( array( 'email_address', 'password' ) as $id ) { $field = $this->find_block( $id, $blocks ); $blocks = $field ? $this->{"toggle_for_$id"}( $field, $blocks ) : $blocks; } return $blocks; } /** * Maybe add the required email and password block to a form. * * @since 5.1.0 * @since 5.4.1 Make sure added reusable blocks contain the actual required field, * otherwise fall back on the dynamically generated ones. * * @param array[] $blocks Array of parsed WP_Block arrays. * @param string $location The request form location ID. * @param array $args Additional arguments passed to the short-circuit filter. * @return array[] */ public function maybe_add_required_block_fields( $blocks, $location, $args ) { $fields_to_require = $this->get_required_fields_for_location( $location, $args ); if ( empty( $fields_to_require ) ) { return $blocks; } foreach ( $fields_to_require as $field_id => $field_block_name ) { $block = $this->find_block( $field_id, $blocks ); if ( ! empty( $block ) ) { // Fields in non checkout forms are always visible - see LLMS_Forms::get_form_html(). $blocks = 'checkout' === $location ? $this->make_block_visible( $block[1], $blocks, $block[0] ) : $blocks; unset( $fields_to_require[ $field_id ] ); if ( empty( $fields_to_require ) ) { // All the required blocks are present. return $blocks; } } } return $this->add_required_block_fields( $fields_to_require, $blocks, $location ); } /** * Add required block fields. * * @since 5.4.1 * * @param string[] $fields_to_require Array of field ids to require. * @param array[] $blocks Array of parsed WP_Block arrays to add required fields to. * @param string $location The request form location ID. * @return array[] */ private function add_required_block_fields( $fields_to_require, $blocks, $location ) { $blocks_to_add = array(); foreach ( $fields_to_require as $field_id => $block_to_add ) { // If a reusable block exists for the field, use it. Otherwise use a dynamically generated block from the template schema. $use_reusable = LLMS_Form_Templates::find_reusable_block( $block_to_add ); $block = LLMS_Form_Templates::get_block( $block_to_add, $location, $use_reusable ); if ( $use_reusable ) { // Load reusable block. $_blocks = LLMS_Forms::instance()->load_reusable_blocks( array( $block ) ); // The reusable block doesn't contain the needed block, use a dynamically generated block from the template schema. if ( empty( $_blocks ) || ! $this->find_block( $field_id, $_blocks ) ) { $_blocks = array( LLMS_Form_Templates::get_block( $block_to_add, $location, false ) ); } $block = $_blocks[0]; } $blocks_to_add[] = $block; } // Make blocks to add visible. $blocks_to_add = 'checkout' === $location ? array_map( array( $this, 'make_all_visible' ), $blocks_to_add ) : $blocks_to_add; return array_merge( $blocks, $blocks_to_add ); } /** * Make a block visible within its list of blocks * * @since 5.1.0 * * @param array $block Parsed WP_Block array. * @param array[] $blocks Array of parsed WP_Block arrays. * @param int $block_index Index of the block within the `$blocks` list. * If the block is in a group, this is the the index of the item's parent. * @return array[] */ private function make_block_visible( $block, $blocks, $block_index ) { if ( LLMS_Forms::instance()->is_block_visible_in_list( $block, array( $blocks[ $block_index ] ) ) ) { return $blocks; } // If the block has a confirm group, use that. $confirm = $this->get_confirm_group( $block['attrs']['id'], array( $blocks[ $block_index ] ) ); $block_to_add = empty( $confirm ) ? $block : $confirm; $replace = true; // Insert the visible block before the invisible one if the block is in a group, // so to avoid the replacement of the whole group which might contain other required fields. // But replace the invisible with the visible if otherwise. if ( $block_to_add !== $blocks[ $block_index ] ) { $replace = false; $this->remove_block( $block_to_add, $blocks ); } // Make the block to add and its children visible. $block_to_add = $this->make_all_visible( $block_to_add ); array_splice( $blocks, $block_index, (int) ( ! empty( $replace ) ), array( $block_to_add ) ); return $blocks; } /** * Remove block from the list which contains it. * * @since 5.1.0 * * @param array $block Parsed WP_Block array. * @param array[] $blocks Array of parsed WP_Block arrays (passed by reference). * @param array $parent Optional. Parsed WP_Block array representing the parent block of the `$blocks`, in case this is a list of inner blocks. Default null. * Passed by reference. * @return bool */ private function remove_block( $block, &$blocks, &$parent = null ) { foreach ( $blocks as $index => &$_block ) { if ( $_block === $block ) { array_splice( $blocks, $index, 1 ); // Remove and re-index. // If we're removing an innerBlock we need to update the innerContent too, to avoid wp calling the render method on nulls. if ( ! is_null( $parent ) ) { $this->remove_inner_block_from_inner_content( $index, $parent ); } return true; } if ( ! empty( $_block['innerBlocks'] ) ) { $removed = $this->remove_block( $block, $_block['innerBlocks'], $_block ); } if ( ! empty( $removed ) ) { // Break as soon as the desired block is removed from one of the innerBlocks. return true; } } return false; } /** * Remove inner block reference from inner content * * See WP_Block::inner_content documentation. * * The inner_content block's property is an array of string fragments and null markers where inner blocks were found. * So here we cycle over the block's parent innerContent field looking for references to innerBlocks (null). * When we found a positional correspondance between the removed innerBlock and its refernce in innerContent we remove the latter too. * * @since 5.1.0 * * @param int $inner_block_index The index of the inner block in the block's innerBlocks list. * @param array $parent Parsed WP_Block array representing the inner blocks parent. Passed by reference. */ private function remove_inner_block_from_inner_content( $inner_block_index, &$parent ) { $inner_block_in_content_index = 0; foreach ( $parent['innerContent'] as $chunk_index => $chunk ) { if ( ! is_string( $chunk ) && $inner_block_index === $inner_block_in_content_index++ ) { array_splice( $parent['innerContent'], $chunk_index, 1 ); // Remove and re-index. break; } } } /** * Make the block and its children visible * * @since 5.1.0 * * @param array $block A parsed WP_Block. * @return array */ private function make_all_visible( $block ) { if ( ! empty( $block['innerBlocks'] ) ) { foreach ( $block['innerBlocks'] as $index => $inner_block ) { $block['innerBlocks'][ $index ] = $this->make_all_visible( $inner_block ); } } $block['attrs']['llms_visibility'] = ''; return $block; } /** * Get confirm group in a list of blocks for a given block id * * @since 5.1.0 * * @param string $id The ID of the field to find the confirm group for. * @param array[] $blocks WP_Block list. * @return array */ private function get_confirm_group( $id, $blocks ) { foreach ( $blocks as $index => $block ) { if ( $block['innerBlocks'] ) { if ( ( 'llms/form-field-confirm-group' === $block['blockName'] ) && $this->find_block( $id, $block['innerBlocks'] ) ) { return $block; } $inner = $this->get_confirm_group( $id, $block['innerBlocks'] ); if ( false !== $inner ) { return $inner; } } } return false; } /** * Modifies block settings for toggle-controlled fields * * @since 5.0.0 * * @param array[] $blocks Array of WP_Block arrays. * @return array[] */ private function modify_toggle_blocks( $blocks ) { // List of toggle fields to modify. $fields = array( 'email_address', 'email_address_confirm', 'password', 'password_confirm', ); foreach ( $blocks as &$block ) { if ( ! empty( $block['innerBlocks'] ) ) { $block['innerBlocks'] = $this->modify_toggle_blocks( $block['innerBlocks'] ); } elseif ( ! empty( $block['attrs']['id'] ) && in_array( $block['attrs']['id'], $fields, true ) ) { $block['attrs']['wrapper_classes'] = 'llms-visually-hidden-field'; $block['attrs']['disabled'] = true; $block['attrs']['required'] = true; } } return $blocks; } /** * Adds a toggle link button allowing the user to change their email address * * @since 5.0.0 * * @param array $email Email field data as located by LLMS_Forms_Dynamic_Fields::find_block(). * @param array[] $blocks Array of WP_Block arrays. * @return array[] */ private function toggle_for_email_address( $email, $blocks ) { return $this->add_block( $blocks, array( 'type' => 'html', 'id' => 'llms-field-toggle--email', 'value' => $this->get_toggle_button_html( '#email_address,#email_address_confirm', $email[1]['attrs']['label'] ), ), $email[0] ); } /** * Adds a current password field and a toggle link button allowing the user to change their password * * @since 5.0.0 * * @param array $password Password field data as located by LLMS_Forms_Dynamic_Fields::find_block(). * @param array[] $blocks Array of WP_Block arrays. * @return array[] */ private function toggle_for_password( $password, $blocks ) { // Add the toggle button. $blocks = $this->add_block( $blocks, array( 'type' => 'html', 'id' => 'llms-field-toggle--password', 'value' => $this->get_toggle_button_html( '#password,#password_confirm,#llms-password-strength-meter,#password_current', $password[1]['attrs']['label'] ), ), $password[1]['attrs']['meter'] ? $password[0] + 1 : $password[0] ); /** * Filters the settings used to create the dynamic password strength meter block * * @since 5.0.0 * * @param array $settings Array or block attributes/settings. */ $current_password = apply_filters( 'llms_current_password_field_settings', array( 'type' => 'password', 'id' => 'password_current', 'name' => 'password_current', 'label' => sprintf( __( 'Current %s', 'lifterlms' ), $password[1]['attrs']['label'] ), 'required' => true, 'disabled' => true, 'data_store_key' => false, 'wrapper_classes' => 'llms-visually-hidden-field', ) ); return $this->add_block( $blocks, $current_password, $password[0] - 1 ); } }
Expand full source code Collapse full source code View on GitHub
Methods Methods
- __construct — Constructor
- add_block — Creates a new HTML block with the given settings and inserts it into an existing blocks array at the specified location
- add_password_strength_meter — Adds a password strength meter to a block list
- add_required_block_fields — Add required block fields.
- find_block — Finds a block with the specified ID within a list of blocks
- get_confirm_group — Get confirm group in a list of blocks for a given block id
- get_required_fields_for_location — Retrieve the fields required for a given location based on user state
- get_toggle_button_html — Retrieve the HTML for a field toggle button link
- make_all_visible — Make the block and its children visible
- make_block_visible — Make a block visible within its list of blocks
- maybe_add_required_block_fields — Maybe add the required email and password block to a form.
- modify_account_form — Modifies account form to improve the UX of editing the email address and password fields
- modify_toggle_blocks — Modifies block settings for toggle-controlled fields
- remove_block — Remove block from the list which contains it.
- remove_inner_block_from_inner_content — Remove inner block reference from inner content
- toggle_for_email_address — Adds a toggle link button allowing the user to change their email address
- toggle_for_password — Adds a current password field and a toggle link button allowing the user to change their password
Changelog Changelog
Version | Description |
---|---|
5.0.0 | Introduced. |