From 7c7bfebd76faea65c2dd9453f4d2987d3080457e Mon Sep 17 00:00:00 2001 From: joeyksclark Date: Wed, 26 Jun 2024 00:48:38 -0700 Subject: [PATCH 1/8] Add password show/hide button in Typescript --- ui/bits/css/_auth.scss | 14 ++++++++++++++ ui/bits/src/bits.login.ts | 19 ++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/ui/bits/css/_auth.scss b/ui/bits/css/_auth.scss index bce6ed4c6ab7..947c36920e1c 100644 --- a/ui/bits/css/_auth.scss +++ b/ui/bits/css/_auth.scss @@ -64,3 +64,17 @@ margin-top: 1rem; } } + +.password-wrapper { + position: relative; +} + +.show-hide-password { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + cursor: pointer; +} diff --git a/ui/bits/src/bits.login.ts b/ui/bits/src/bits.login.ts index 901119e530ff..f72323e481f8 100644 --- a/ui/bits/src/bits.login.ts +++ b/ui/bits/src/bits.login.ts @@ -25,7 +25,7 @@ function loginStart() { const toggleSubmit = ($submit: Cash, v: boolean) => $submit.prop('disabled', !v).toggleClass('disabled', !v); - + passwordShowHide(); (function load() { const form = document.querySelector(selector) as HTMLFormElement, $f = $(form); @@ -110,3 +110,20 @@ function signupStart() { site.asset.loadEsm('bits.passwordComplexity', { init: 'form3-password' }); } + +function passwordShowHide() { + $('#form3-password').each(function (this: HTMLElement) { + const $input = $(this); + $input.wrap('
'); + const $wrapper = $input.parent(); + const $button = $('').appendTo( + $wrapper, + ); + $button.on('click', function (e: Event) { + e.preventDefault(); + const type = $input.attr('type') === 'password' ? 'text' : 'password'; + $input.attr('type', type); + $button.toggleClass('show', type === 'text'); + }); + }); +} From 91b7295d0959a88d402dd3e2eb5f756b888cbe36 Mon Sep 17 00:00:00 2001 From: joeyksclark Date: Wed, 26 Jun 2024 01:11:16 -0700 Subject: [PATCH 2/8] Move UI element definitions to scala --- modules/web/src/main/ui/AuthUi.scala | 11 ++++++----- ui/bits/css/_auth.scss | 1 - ui/bits/src/bits.login.ts | 12 +++++------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/modules/web/src/main/ui/AuthUi.scala b/modules/web/src/main/ui/AuthUi.scala index 587d40472d72..3e3b5ea4befe 100644 --- a/modules/web/src/main/ui/AuthUi.scala +++ b/modules/web/src/main/ui/AuthUi.scala @@ -337,9 +337,7 @@ body { margin-top: 45px; } "policy" -> trans.site.agreementPolicy() ) - private def formFields(username: Field, password: Field, email: Option[Field], register: Boolean)(using - Context - ) = + private def formFields(username: Field, password: Field, email: Option[Field], register: Boolean)(using Context) = frag( form3.group( username, @@ -350,8 +348,11 @@ body { margin-top: 45px; } form3.input(f)(autofocus, required, autocomplete := "username"), register.option(p(cls := "error username-exists none")(trans.site.usernameAlreadyUsed())) ), - form3.passwordModified(password, trans.site.password())( - autocomplete := (if register then "new-password" else "current-password") + div(cls := "password-wrapper")( + form3.passwordModified(password, trans.site.password())( + autocomplete := (if register then "new-password" else "current-password") + ), + button(cls := "show-hide-password", title := "Show/hide password")("Joey") ), register.option(form3.passwordComplexityMeter(trans.site.newPasswordStrength())), email.map: email => diff --git a/ui/bits/css/_auth.scss b/ui/bits/css/_auth.scss index 947c36920e1c..21f72c379fd4 100644 --- a/ui/bits/css/_auth.scss +++ b/ui/bits/css/_auth.scss @@ -73,7 +73,6 @@ position: absolute; right: 10px; top: 50%; - transform: translateY(-50%); background: none; border: none; cursor: pointer; diff --git a/ui/bits/src/bits.login.ts b/ui/bits/src/bits.login.ts index f72323e481f8..129e38ee5fb8 100644 --- a/ui/bits/src/bits.login.ts +++ b/ui/bits/src/bits.login.ts @@ -82,6 +82,7 @@ function loginStart() { } function signupStart() { + passwordShowHide(); const $form = $('#signup-form'), $exists = $form.find('.username-exists'), $username = $form.find('input[name="username"]').on('change keyup paste', () => { @@ -112,13 +113,10 @@ function signupStart() { } function passwordShowHide() { - $('#form3-password').each(function (this: HTMLElement) { - const $input = $(this); - $input.wrap('
'); - const $wrapper = $input.parent(); - const $button = $('').appendTo( - $wrapper, - ); + $('.password-wrapper').each(function (this: HTMLElement) { + const $wrapper = $(this); + const $input = $wrapper.find('input[type="password"], input[type="text"]'); + const $button = $wrapper.find('.show-hide-password'); $button.on('click', function (e: Event) { e.preventDefault(); const type = $input.attr('type') === 'password' ? 'text' : 'password'; From cf4f1e0bcb77e574e7349d9fe4bb7f41f9de9a88 Mon Sep 17 00:00:00 2001 From: joeyksclark Date: Wed, 26 Jun 2024 01:39:57 -0700 Subject: [PATCH 3/8] Use eye icon instead of button --- modules/web/src/main/ui/AuthUi.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/web/src/main/ui/AuthUi.scala b/modules/web/src/main/ui/AuthUi.scala index 3e3b5ea4befe..d76567e44f6d 100644 --- a/modules/web/src/main/ui/AuthUi.scala +++ b/modules/web/src/main/ui/AuthUi.scala @@ -352,7 +352,9 @@ body { margin-top: 45px; } form3.passwordModified(password, trans.site.password())( autocomplete := (if register then "new-password" else "current-password") ), - button(cls := "show-hide-password", title := "Show/hide password")("Joey") + button(cls := "show-hide-password")( + i(dataIcon := Icon.Eye) + ) ), register.option(form3.passwordComplexityMeter(trans.site.newPasswordStrength())), email.map: email => From 4fd9ce6375deeece5bc13c2b4ffbc17c49447a6f Mon Sep 17 00:00:00 2001 From: joeyksclark Date: Wed, 26 Jun 2024 01:47:07 -0700 Subject: [PATCH 4/8] Add/remove strikethrough on password toggle --- ui/bits/css/_auth.scss | 11 +++++++++++ ui/bits/src/bits.login.ts | 1 + 2 files changed, 12 insertions(+) diff --git a/ui/bits/css/_auth.scss b/ui/bits/css/_auth.scss index 21f72c379fd4..bf8ad9bab921 100644 --- a/ui/bits/css/_auth.scss +++ b/ui/bits/css/_auth.scss @@ -77,3 +77,14 @@ border: none; cursor: pointer; } + +.show-hide-password.strikethrough::before { + content: ''; + position: absolute; + width: 100%; + height: 2px; + background-color: currentColor; + transform: rotate(45deg); + top: 50%; + left: 0; +} diff --git a/ui/bits/src/bits.login.ts b/ui/bits/src/bits.login.ts index 129e38ee5fb8..3d5a59fb4513 100644 --- a/ui/bits/src/bits.login.ts +++ b/ui/bits/src/bits.login.ts @@ -122,6 +122,7 @@ function passwordShowHide() { const type = $input.attr('type') === 'password' ? 'text' : 'password'; $input.attr('type', type); $button.toggleClass('show', type === 'text'); + $button.toggleClass('strikethrough'); }); }); } From 1a4e881ffdeb54240a73d0707ab616522918ceaa Mon Sep 17 00:00:00 2001 From: joeyksclark Date: Wed, 26 Jun 2024 01:56:56 -0700 Subject: [PATCH 5/8] Cleanup --- ui/bits/src/bits.login.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ui/bits/src/bits.login.ts b/ui/bits/src/bits.login.ts index 3d5a59fb4513..2e902ef59a3f 100644 --- a/ui/bits/src/bits.login.ts +++ b/ui/bits/src/bits.login.ts @@ -25,7 +25,6 @@ function loginStart() { const toggleSubmit = ($submit: Cash, v: boolean) => $submit.prop('disabled', !v).toggleClass('disabled', !v); - passwordShowHide(); (function load() { const form = document.querySelector(selector) as HTMLFormElement, $f = $(form); @@ -79,10 +78,11 @@ function loginStart() { }); }); })(); + + addPasswordVisibilityToggleListener(); } function signupStart() { - passwordShowHide(); const $form = $('#signup-form'), $exists = $form.find('.username-exists'), $username = $form.find('input[name="username"]').on('change keyup paste', () => { @@ -110,9 +110,11 @@ function signupStart() { }); site.asset.loadEsm('bits.passwordComplexity', { init: 'form3-password' }); + + addPasswordVisibilityToggleListener(); } -function passwordShowHide() { +function addPasswordVisibilityToggleListener() { $('.password-wrapper').each(function (this: HTMLElement) { const $wrapper = $(this); const $input = $wrapper.find('input[type="password"], input[type="text"]'); From 8ed4e83258204befcb429d0d11743b4b61eeda87 Mon Sep 17 00:00:00 2001 From: joeyksclark Date: Wed, 26 Jun 2024 02:12:54 -0700 Subject: [PATCH 6/8] Run scala linter --- modules/web/src/main/ui/AuthUi.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/web/src/main/ui/AuthUi.scala b/modules/web/src/main/ui/AuthUi.scala index d76567e44f6d..7579d1116c0c 100644 --- a/modules/web/src/main/ui/AuthUi.scala +++ b/modules/web/src/main/ui/AuthUi.scala @@ -337,7 +337,9 @@ body { margin-top: 45px; } "policy" -> trans.site.agreementPolicy() ) - private def formFields(username: Field, password: Field, email: Option[Field], register: Boolean)(using Context) = + private def formFields(username: Field, password: Field, email: Option[Field], register: Boolean)(using + Context + ) = frag( form3.group( username, From 71b2b35a17367ed4baad4a6524d8aceeb36c703a Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 26 Jun 2024 14:16:33 +0200 Subject: [PATCH 7/8] password show/hide simplify scala,html,scss,ts --- modules/ui/src/main/helper/Form3.scala | 6 +++++- modules/web/src/main/ui/AuthUi.scala | 9 ++------- ui/bits/css/_auth.scss | 26 ++++++-------------------- ui/bits/src/bits.login.ts | 5 ++--- 4 files changed, 15 insertions(+), 31 deletions(-) diff --git a/modules/ui/src/main/helper/Form3.scala b/modules/ui/src/main/helper/Form3.scala index 1430d89156ab..86e66e0a0a92 100644 --- a/modules/ui/src/main/helper/Form3.scala +++ b/modules/ui/src/main/helper/Form3.scala @@ -179,7 +179,11 @@ final class Form3(formHelper: FormHelper & I18nHelper, flairApi: FlairApi): def hiddenFalse(field: Field): Tag = hidden(field, "false".some) def passwordModified(field: Field, content: Frag)(modifiers: Modifier*)(using Translate): Frag = - group(field, content)(input(_, typ = "password")(required)(modifiers)) + group(field, content): f => + div(cls := "password-wrapper")( + input(f, typ = "password")(required)(modifiers), + button(cls := "show-hide-password", dataIcon := Icon.Eye) + ) def passwordComplexityMeter(labelContent: Frag): Frag = div(cls := "password-complexity")( diff --git a/modules/web/src/main/ui/AuthUi.scala b/modules/web/src/main/ui/AuthUi.scala index 7579d1116c0c..587d40472d72 100644 --- a/modules/web/src/main/ui/AuthUi.scala +++ b/modules/web/src/main/ui/AuthUi.scala @@ -350,13 +350,8 @@ body { margin-top: 45px; } form3.input(f)(autofocus, required, autocomplete := "username"), register.option(p(cls := "error username-exists none")(trans.site.usernameAlreadyUsed())) ), - div(cls := "password-wrapper")( - form3.passwordModified(password, trans.site.password())( - autocomplete := (if register then "new-password" else "current-password") - ), - button(cls := "show-hide-password")( - i(dataIcon := Icon.Eye) - ) + form3.passwordModified(password, trans.site.password())( + autocomplete := (if register then "new-password" else "current-password") ), register.option(form3.passwordComplexityMeter(trans.site.newPasswordStrength())), email.map: email => diff --git a/ui/bits/css/_auth.scss b/ui/bits/css/_auth.scss index bf8ad9bab921..f7e1bfa5070f 100644 --- a/ui/bits/css/_auth.scss +++ b/ui/bits/css/_auth.scss @@ -65,26 +65,12 @@ } } -.password-wrapper { - position: relative; -} - .show-hide-password { - position: absolute; - right: 10px; - top: 50%; - background: none; - border: none; - cursor: pointer; + @extend %button-none; + float: right; + margin-right: 1em; + margin-top: -2.2em; } - -.show-hide-password.strikethrough::before { - content: ''; - position: absolute; - width: 100%; - height: 2px; - background-color: currentColor; - transform: rotate(45deg); - top: 50%; - left: 0; +.show-hide-password.revealed { + color: $c-bad; } diff --git a/ui/bits/src/bits.login.ts b/ui/bits/src/bits.login.ts index 2e902ef59a3f..5725f3bed5b3 100644 --- a/ui/bits/src/bits.login.ts +++ b/ui/bits/src/bits.login.ts @@ -117,14 +117,13 @@ function signupStart() { function addPasswordVisibilityToggleListener() { $('.password-wrapper').each(function (this: HTMLElement) { const $wrapper = $(this); - const $input = $wrapper.find('input[type="password"], input[type="text"]'); const $button = $wrapper.find('.show-hide-password'); $button.on('click', function (e: Event) { e.preventDefault(); + const $input = $wrapper.find('input'); const type = $input.attr('type') === 'password' ? 'text' : 'password'; $input.attr('type', type); - $button.toggleClass('show', type === 'text'); - $button.toggleClass('strikethrough'); + $button.toggleClass('revealed', type == 'text'); }); }); } From 2de5624af5e528bb13cd5619a9d05614dedaa4a8 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 26 Jun 2024 14:36:42 +0200 Subject: [PATCH 8/8] add password reveal to many other pages such as /account/twofactor, /account/close, /account/passwd and more --- modules/team/src/main/ui/RequestUi.scala | 2 +- modules/ui/src/main/helper/Form3.scala | 6 ++++-- ui/bits/css/_auth.scss | 10 ---------- ui/bits/css/build/bits.account.scss | 1 + ui/bits/css/build/bits.auth.scss | 1 + ui/bits/src/bits.account.ts | 3 +++ ui/bits/src/bits.login.ts | 21 +++----------------- ui/common/css/form/_form3.scss | 17 ---------------- ui/common/css/form/_password.scss | 25 ++++++++++++++++++++++++ ui/common/src/password.ts | 13 ++++++++++++ 10 files changed, 51 insertions(+), 48 deletions(-) create mode 100644 ui/common/css/form/_password.scss create mode 100644 ui/common/src/password.ts diff --git a/modules/team/src/main/ui/RequestUi.scala b/modules/team/src/main/ui/RequestUi.scala index 1b511ca2c8dd..28f00b11f4a6 100644 --- a/modules/team/src/main/ui/RequestUi.scala +++ b/modules/team/src/main/ui/RequestUi.scala @@ -27,7 +27,7 @@ final class RequestUi(helpers: Helpers, bits: TeamUi): ) ), t.password.nonEmpty.so( - form3.passwordModified(form("password"), trt.entryCode())( + form3.passwordModified(form("password"), trt.entryCode(), reveal = false)( autocomplete := "new-password" ) ), diff --git a/modules/ui/src/main/helper/Form3.scala b/modules/ui/src/main/helper/Form3.scala index 86e66e0a0a92..cfbfa45ad70b 100644 --- a/modules/ui/src/main/helper/Form3.scala +++ b/modules/ui/src/main/helper/Form3.scala @@ -178,11 +178,13 @@ final class Form3(formHelper: FormHelper & I18nHelper, flairApi: FlairApi): // allows disabling of a field that defaults to true def hiddenFalse(field: Field): Tag = hidden(field, "false".some) - def passwordModified(field: Field, content: Frag)(modifiers: Modifier*)(using Translate): Frag = + def passwordModified(field: Field, content: Frag, reveal: Boolean = true)( + modifiers: Modifier* + )(using Translate): Frag = group(field, content): f => div(cls := "password-wrapper")( input(f, typ = "password")(required)(modifiers), - button(cls := "show-hide-password", dataIcon := Icon.Eye) + reveal.option(button(cls := "password-reveal", dataIcon := Icon.Eye)) ) def passwordComplexityMeter(labelContent: Frag): Frag = diff --git a/ui/bits/css/_auth.scss b/ui/bits/css/_auth.scss index f7e1bfa5070f..bce6ed4c6ab7 100644 --- a/ui/bits/css/_auth.scss +++ b/ui/bits/css/_auth.scss @@ -64,13 +64,3 @@ margin-top: 1rem; } } - -.show-hide-password { - @extend %button-none; - float: right; - margin-right: 1em; - margin-top: -2.2em; -} -.show-hide-password.revealed { - color: $c-bad; -} diff --git a/ui/bits/css/build/bits.account.scss b/ui/bits/css/build/bits.account.scss index bede02bec5eb..fc6680e347e5 100644 --- a/ui/bits/css/build/bits.account.scss +++ b/ui/bits/css/build/bits.account.scss @@ -3,4 +3,5 @@ @import '../../../common/css/form/form3'; @import '../../../common/css/form/radio'; @import '../../../common/css/form/emoji-picker'; +@import '../../../common/css/form/password'; @import '../account'; diff --git a/ui/bits/css/build/bits.auth.scss b/ui/bits/css/build/bits.auth.scss index 6aae96927a3d..487fae78396b 100644 --- a/ui/bits/css/build/bits.auth.scss +++ b/ui/bits/css/build/bits.auth.scss @@ -1,4 +1,5 @@ @import '../../../common/css/plugin'; @import '../../../common/css/form/form3'; @import '../../../common/css/form/captcha'; +@import '../../../common/css/form/password'; @import '../auth'; diff --git a/ui/bits/src/bits.account.ts b/ui/bits/src/bits.account.ts index a187550c826b..9dab290051bb 100644 --- a/ui/bits/src/bits.account.ts +++ b/ui/bits/src/bits.account.ts @@ -1,5 +1,6 @@ import * as licon from 'common/licon'; import * as xhr from 'common/xhr'; +import { addPasswordVisibilityToggleListener } from 'common/password'; import flairPicker from './load/flairPicker'; site.load.then(() => { @@ -7,6 +8,8 @@ site.load.then(() => { flairPicker(this); }); + addPasswordVisibilityToggleListener(); + const localPrefs: [string, string, string, boolean][] = [ ['behavior', 'arrowSnap', 'arrow.snap', true], ['behavior', 'courtesy', 'courtesy', false], diff --git a/ui/bits/src/bits.login.ts b/ui/bits/src/bits.login.ts index 5725f3bed5b3..3fd5d4a7f319 100644 --- a/ui/bits/src/bits.login.ts +++ b/ui/bits/src/bits.login.ts @@ -1,9 +1,12 @@ import * as xhr from 'common/xhr'; import debounce from 'common/debounce'; +import { addPasswordVisibilityToggleListener } from 'common/password'; import { storedJsonProp } from 'common/storage'; export function initModule(mode: 'login' | 'signup') { mode === 'login' ? loginStart() : signupStart(); + + addPasswordVisibilityToggleListener(); } class LoginHistory { historyStorage = storedJsonProp('login.history', () => []); @@ -78,8 +81,6 @@ function loginStart() { }); }); })(); - - addPasswordVisibilityToggleListener(); } function signupStart() { @@ -110,20 +111,4 @@ function signupStart() { }); site.asset.loadEsm('bits.passwordComplexity', { init: 'form3-password' }); - - addPasswordVisibilityToggleListener(); -} - -function addPasswordVisibilityToggleListener() { - $('.password-wrapper').each(function (this: HTMLElement) { - const $wrapper = $(this); - const $button = $wrapper.find('.show-hide-password'); - $button.on('click', function (e: Event) { - e.preventDefault(); - const $input = $wrapper.find('input'); - const type = $input.attr('type') === 'password' ? 'text' : 'password'; - $input.attr('type', type); - $button.toggleClass('revealed', type == 'text'); - }); - }); } diff --git a/ui/common/css/form/_form3.scss b/ui/common/css/form/_form3.scss index d0c92b3062cc..1cab3475eb0f 100644 --- a/ui/common/css/form/_form3.scss +++ b/ui/common/css/form/_form3.scss @@ -114,23 +114,6 @@ textarea.form-control { border-top: $border; } -.password-complexity { - margin-top: -2rem; - margin-bottom: 3rem; -} - -.password-complexity-meter { - display: flex; - grid-gap: 0.25rem; - height: 0.4rem; - margin-top: 1rem; - - > * { - background-color: gray; - width: 25%; - } -} - .form-fieldset { @extend %box-radius; margin: 1rem 0 3rem 0; diff --git a/ui/common/css/form/_password.scss b/ui/common/css/form/_password.scss new file mode 100644 index 000000000000..f7c2cb3385eb --- /dev/null +++ b/ui/common/css/form/_password.scss @@ -0,0 +1,25 @@ +.password-complexity { + margin-top: -2rem; + margin-bottom: 3rem; +} +.password-complexity-meter { + display: flex; + grid-gap: 0.25rem; + height: 0.4rem; + margin-top: 1rem; + + > * { + background-color: gray; + width: 25%; + } +} + +.password-reveal { + @extend %button-none; + float: right; + margin-right: 1em; + margin-top: -2.2em; +} +.password-reveal.revealed { + color: $c-bad; +} diff --git a/ui/common/src/password.ts b/ui/common/src/password.ts new file mode 100644 index 000000000000..5c6eb7135a5a --- /dev/null +++ b/ui/common/src/password.ts @@ -0,0 +1,13 @@ +export const addPasswordVisibilityToggleListener = () => { + $('.password-wrapper').each(function (this: HTMLElement) { + const $wrapper = $(this); + const $button = $wrapper.find('.password-reveal'); + $button.on('click', function (e: Event) { + e.preventDefault(); + const $input = $wrapper.find('input'); + const type = $input.attr('type') === 'password' ? 'text' : 'password'; + $input.attr('type', type); + $button.toggleClass('revealed', type == 'text'); + }); + }); +};