From 12f1ee196ef5d59d7af86f8a0274de660cd9ffcf Mon Sep 17 00:00:00 2001 From: Kim Coleman Date: Fri, 1 Nov 2024 05:57:46 -0400 Subject: [PATCH 1/2] Initial work to add password visibility toggle button on Student Dashboard login form --- assets/js/app/llms-visibility-toggle.js | 76 ++++++++++++++ assets/scss/_includes/_buttons.scss | 25 +++++ assets/scss/_includes/_llms-form-field.scss | 22 ++++ includes/class.llms.person.handler.php | 30 +++--- includes/forms/class-llms-form-field.php | 108 +++++++++++--------- 5 files changed, 201 insertions(+), 60 deletions(-) create mode 100644 assets/js/app/llms-visibility-toggle.js diff --git a/assets/js/app/llms-visibility-toggle.js b/assets/js/app/llms-visibility-toggle.js new file mode 100644 index 0000000000..35674710b5 --- /dev/null +++ b/assets/js/app/llms-visibility-toggle.js @@ -0,0 +1,76 @@ +/** + * Handle Password Visibility Toggle for LifterLMS Forms + * + * @package LifterLMS/Scripts + * + * @since TBD + */ + +LLMS.PasswordVisibility = { + + /** + * Initialize references and setup event binding + * + * @since TBD + * @return void + */ + init: function() { + this.$toggleButtons = $( '.llms-visibility-toggle button' ); + + if ( this.$toggleButtons.length ) { + this.$toggleButtons.removeClass( 'hide-if-no-js' ); + this.bind(); + } + }, + + /** + * Bind DOM events for toggle buttons + * + * @since TBD + * @return void + */ + bind: function() { + var self = this; + + // Remove any previous click events and bind the new click event + this.$toggleButtons.off('click').on('click', function(event) { + self.toggleVisibility( $(this) ); + }); + }, + + /** + * Toggle visibility of password fields + * + * @since TBD + * @param {Object} $button The jQuery object of the clicked button + * @return void + */ + toggleVisibility: function( $button ) { + var isVisible = parseInt( $button.attr('data-toggle'), 10 ); + var $form = $button.closest( '.llms-form-fields' ); + var $passwordFields = $form.find( 'input.llms-field-input' ); + var $icon = $button.find( 'i' ); + var $stateText = $button.find( '.llms-visibility-toggle-state' ); + + // Toggle the visibility state + if ( isVisible === 1 ) { + // Show password + $passwordFields.filter('[type="password"]').attr('type', 'text'); + $button.attr('data-toggle', 0); + $icon.removeClass('fa-eye').addClass('fa-eye-slash'); + $stateText.text(LLMS.l10n.translate('Hide Password')); + } else { + // Hide password + $passwordFields.filter('[type="text"]').attr('type', 'password'); + $button.attr('data-toggle', 1); + $icon.removeClass('fa-eye-slash').addClass('fa-eye'); + $stateText.text(LLMS.l10n.translate('Show Password')); + } + } + +}; + +// Initialize the Password Visibility module on document ready +jQuery(document).ready(function($) { + LLMS.PasswordVisibility.init(); +}); diff --git a/assets/scss/_includes/_buttons.scss b/assets/scss/_includes/_buttons.scss index 41911310d0..abdac635e9 100644 --- a/assets/scss/_includes/_buttons.scss +++ b/assets/scss/_includes/_buttons.scss @@ -164,6 +164,31 @@ a.llms-button-secondary { } +.llms-button-plain { + background: transparent; + border: none; + border-radius: 3px; + color: #1D2327; + cursor: pointer; + font-size: 16px; + font-weight: 700; + text-decoration: none; + text-shadow: none; + line-height: 1; + margin: 0; + max-width: 100%; + padding: 1px 3px; + position: relative; + + &:hover, &:active { + color: #1D2327; + } + &:focus { + color: #1D2327; + } + +} + .llms-course-continue-button { display: inline-block; } diff --git a/assets/scss/_includes/_llms-form-field.scss b/assets/scss/_includes/_llms-form-field.scss index d6fb817cd6..eb063e10b4 100644 --- a/assets/scss/_includes/_llms-form-field.scss +++ b/assets/scss/_includes/_llms-form-field.scss @@ -215,6 +215,28 @@ height: 100%; } + &:has(.llms-visibility-toggle) { + align-items: center; + display: grid; + grid-template-areas: + "label toggle" + "input input"; + grid-template-columns: 1fr auto; + + label { + grid-area: label; + } + + input { + grid-area: input; + } + + .llms-visibility-toggle { + grid-area: toggle; + justify-self: end; + } + } + } diff --git a/includes/class.llms.person.handler.php b/includes/class.llms.person.handler.php index 47efa3aa9d..05672881c8 100644 --- a/includes/class.llms.person.handler.php +++ b/includes/class.llms.person.handler.php @@ -166,12 +166,13 @@ public static function get_login_fields( $layout = 'columns' ) { 'type' => ! $usernames ? 'email' : 'text', ), array( - 'columns' => ( 'columns' == $layout ) ? 6 : 12, - 'id' => 'llms_password', - 'label' => __( 'Password', 'lifterlms' ), - 'last_column' => ( 'columns' == $layout ) ? true : true, - 'required' => true, - 'type' => 'password', + 'columns' => ( 'columns' == $layout ) ? 6 : 12, + 'id' => 'llms_password', + 'label' => __( 'Password', 'lifterlms' ), + 'last_column' => ( 'columns' == $layout ) ? true : true, + 'required' => true, + 'type' => 'password', + 'visibility_toggle' => true, ), array( 'columns' => ( 'columns' == $layout ) ? 3 : 12, @@ -280,14 +281,15 @@ private static function get_password_fields() { $fields = array(); $fields[] = array( - 'columns' => 6, - 'classes' => 'llms-password', - 'id' => 'password', - 'label' => __( 'Password', 'lifterlms' ), - 'last_column' => false, - 'match' => 'password_confirm', - 'required' => true, - 'type' => 'password', + 'columns' => 6, + 'classes' => 'llms-password', + 'id' => 'password', + 'label' => __( 'Password', 'lifterlms' ), + 'last_column' => false, + 'match' => 'password_confirm', + 'required' => true, + 'type' => 'password', + 'visibility_toggle' => true, ); $fields[] = array( 'columns' => 6, diff --git a/includes/forms/class-llms-form-field.php b/includes/forms/class-llms-form-field.php index ce5435c1dc..abb4c7122e 100644 --- a/includes/forms/class-llms-form-field.php +++ b/includes/forms/class-llms-form-field.php @@ -23,29 +23,30 @@ class LLMS_Form_Field { * @var array { * Array of field settings. * - * @type array $attributes Associative array of HTML attributes to add to the field element. - * @type bool $checked Determines if radio and checkbox fields are checked. - * @type int $columns Number of columns the field wrapper should occupy when rendered. Accepts integers >= 1 and <= 12. - * @type string[]|string $classes Additional CSS classes to add to the field element. Accepts a string or an array of strings. - * @type string $data_store Determines where to store field values. Accepts "users" or "usermeta" to store on the respective WP core tables. - * @type string|false $data_store_key Determines the key name to use when storing the field value. Pass `false` to disable automatic storage. Defaults to the value of the `$name` property. - * @type string $description A string to use as the field's description or helper text. - * @type string $default The default value to use for the field. - * @type bool $disabled Whether or not the field is enabled. - * @type string $id The field's HTML "id" attribute. Must be unique. If not supplied, an ID is automatically generated. - * @type string $label Text to use in the label element associated with the field. - * @type bool $label_show_empty When true and no `$label` is supplied, will show an empty label element. - * @type bool $last_column When true, outputs a clearfix element following the element's wrapper. Allows ending a "row" of fields. - * @type bool $match Match this field to another field for validation purposes. Must be the `$id` of another field in the form. - * @type string $name The field's HTML "name" attribute. Default's to the value of `$id` when not supplied. - * @type array $options An associative array of options used for select, checkbox groups, and radio fields. - * @type string $options_preset A string representing a pre-defined set of `$options`. Accepts "countries" or "states". Custom presets can be defined using the filter "llms_form_field_options_preset_{$preset_id}". - * @type string $placeholder The field's HTML placeholder attribute. - * @type bool $required Determines if the field is marked as required. - * @type string $selected Alias of `$default`. - * @type string $type Field type. Accepts any HTML5 input type (text, email, tel, etc...), radio, checkbox, select, textarea, button, reset, submit, and html. - * @type string $value Value of the field. - * @type string[]|string $wrapper_classes Additional CSS classes to add to the field's wrapper element. Accepts a string or an array of strings. + * @type array $attributes Associative array of HTML attributes to add to the field element. + * @type bool $checked Determines if radio and checkbox fields are checked. + * @type int $columns Number of columns the field wrapper should occupy when rendered. Accepts integers >= 1 and <= 12. + * @type string[]|string $classes Additional CSS classes to add to the field element. Accepts a string or an array of strings. + * @type string $data_store Determines where to store field values. Accepts "users" or "usermeta" to store on the respective WP core tables. + * @type string|false $data_store_key Determines the key name to use when storing the field value. Pass `false` to disable automatic storage. Defaults to the value of the `$name` property. + * @type string $description A string to use as the field's description or helper text. + * @type string $default The default value to use for the field. + * @type bool $disabled Whether or not the field is enabled. + * @type string $id The field's HTML "id" attribute. Must be unique. If not supplied, an ID is automatically generated. + * @type string $label Text to use in the label element associated with the field. + * @type bool $label_show_empty When true and no `$label` is supplied, will show an empty label element. + * @type bool $last_column When true, outputs a clearfix element following the element's wrapper. Allows ending a "row" of fields. + * @type bool $match Match this field to another field for validation purposes. Must be the `$id` of another field in the form. + * @type string $name The field's HTML "name" attribute. Default's to the value of `$id` when not supplied. + * @type array $options An associative array of options used for select, checkbox groups, and radio fields. + * @type string $options_preset A string representing a pre-defined set of `$options`. Accepts "countries" or "states". Custom presets can be defined using the filter "llms_form_field_options_preset_{$preset_id}". + * @type string $placeholder The field's HTML placeholder attribute. + * @type bool $required Determines if the field is marked as required. + * @type string $selected Alias of `$default`. + * @type string $type Field type. Accepts any HTML5 input type (text, email, tel, etc...), radio, checkbox, select, textarea, button, reset, submit, and html. + * @type string $value Value of the field. + * @type string $visibility_toggle Determines if the field should show a button to toggle field masking (for password fields). + * @type string[]|string $wrapper_classes Additional CSS classes to add to the field's wrapper element. Accepts a string or an array of strings. * } */ protected $settings = array(); @@ -202,29 +203,30 @@ public function explode_options_to_fields( $is_hidden = false ) { protected function get_defaults() { return array( - 'attributes' => array(), - 'checked' => false, - 'columns' => 12, - 'classes' => array(), // Or string of space-separated classes. - 'data_store' => 'usermeta', // Users or usermeta. - 'data_store_key' => '', // Defaults to value passed for "name". - 'description' => '', - 'default' => '', - 'disabled' => false, - 'id' => '', - 'label' => '', - 'label_show_empty' => false, - 'last_column' => true, - 'match' => '', // Test. - 'name' => '', // Defaults to value passed for "id". - 'options' => array(), - 'options_preset' => '', - 'placeholder' => '', - 'required' => false, - 'selected' => '', // Alias of "default". - 'type' => 'text', - 'value' => '', - 'wrapper_classes' => array(), // Or string of space-separated classes. + 'attributes' => array(), + 'checked' => false, + 'columns' => 12, + 'classes' => array(), // Or string of space-separated classes. + 'data_store' => 'usermeta', // Users or usermeta. + 'data_store_key' => '', // Defaults to value passed for "name". + 'description' => '', + 'default' => '', + 'disabled' => false, + 'id' => '', + 'label' => '', + 'label_show_empty' => false, + 'last_column' => true, + 'match' => '', // Test. + 'name' => '', // Defaults to value passed for "id". + 'options' => array(), + 'options_preset' => '', + 'placeholder' => '', + 'required' => false, + 'selected' => '', // Alias of "default". + 'type' => 'text', + 'value' => '', + 'visibility_toggle' => false, + 'wrapper_classes' => array(), // Or string of space-separated classes. ); } @@ -262,6 +264,18 @@ protected function get_description_html() { return $this->settings['description'] ? sprintf( '%s', $this->settings['description'] ) : ''; } + /** + * Retrieve HTML for the visibility toggle button + * + * @since TBD + * + * @return string + */ + protected function get_visibility_toggle_html() { + + return $this->settings['visibility_toggle'] ? '
' : ''; + } + /** * Retrieve the full HTML for the field. * @@ -457,6 +471,8 @@ public function get_html() { $after .= $this->get_description_html(); } + $after .= $this->get_visibility_toggle_html(); + $after .= ''; if ( $this->settings['last_column'] ) { From aacbe84879c24ce7841fcd7a8b4772979e5a7135 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Thu, 9 Jan 2025 10:24:34 -0500 Subject: [PATCH 2/2] Changelog. --- .changelogs/password-toggle.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelogs/password-toggle.yml diff --git a/.changelogs/password-toggle.yml b/.changelogs/password-toggle.yml new file mode 100644 index 0000000000..f5b61d9b8a --- /dev/null +++ b/.changelogs/password-toggle.yml @@ -0,0 +1,3 @@ +significance: patch +type: added +entry: Ability to show typed password for verification.