From 3592861804c484d3386c1be5eb050c1761892c59 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Willms Date: Fri, 14 Feb 2025 11:15:04 +0000 Subject: implement generic methods to allow the user to display a password in a password input, fixes #5186 Closes #5186 Merge request studip/studip!3876 --- app/views/admin/user/edit.php | 13 +++++- app/views/login/_standard_loginform.php | 20 +++------ app/views/settings/password.php | 6 ++- .../assets/javascripts/bootstrap/application.js | 21 ++++----- resources/assets/stylesheets/studip.scss | 20 +++++++++ resources/vue/base-directives.js | 2 + .../vue/components/admission/PasswordAdmission.vue | 18 ++++---- resources/vue/directives/allow-plaintext-toggle.ts | 52 ++++++++++++++++++++++ 8 files changed, 112 insertions(+), 40 deletions(-) create mode 100644 resources/vue/directives/allow-plaintext-toggle.ts diff --git a/app/views/admin/user/edit.php b/app/views/admin/user/edit.php index 3313e6a..d3e0daf 100644 --- a/app/views/admin/user/edit.php +++ b/app/views/admin/user/edit.php @@ -246,13 +246,22 @@ use Studip\Button, Studip\LinkButton; diff --git a/app/views/login/_standard_loginform.php b/app/views/login/_standard_loginform.php index 1353eb8..d772dbd 100644 --- a/app/views/login/_standard_loginform.php +++ b/app/views/login/_standard_loginform.php @@ -27,21 +27,13 @@ $password_tooltip_text = (string) Config::get()->PASSWORD_TOOLTIP_TEXT; id="password" - name="password" autocomplete="current-password" size="20" required placeholder="" + class="allow-plaintext-toggle" + name="password" + autocomplete="current-password" + size="20" + required + placeholder="" > - - - asImg(20, [ - 'id' => 'visible-password', - 'title' => _('Passwort anzeigen'), - ]) ?> - asImg(20, [ - 'id' => 'invisible-password', - 'style' => 'display: none', - 'title' => _('Passwort verstecken'), - ]) ?> - - diff --git a/app/views/settings/password.php b/app/views/settings/password.php index 70846fb..99e4177 100644 --- a/app/views/settings/password.php +++ b/app/views/settings/password.php @@ -9,17 +9,21 @@ diff --git a/resources/assets/javascripts/bootstrap/application.js b/resources/assets/javascripts/bootstrap/application.js index 4e5eccc..7fdb316 100644 --- a/resources/assets/javascripts/bootstrap/application.js +++ b/resources/assets/javascripts/bootstrap/application.js @@ -1,5 +1,7 @@ import { $gettext } from '../lib/gettext'; import eventBus from "../lib/event-bus.ts"; +import allowPlaintextToggle from "../../../vue/directives/allow-plaintext-toggle"; + /* ------------------------------------------------------------------------ * application.js @@ -365,8 +367,6 @@ STUDIP.domReady(function () { const passwordInput = document.getElementById('password'); const usernameInput = document.getElementById('loginname'); const passwordCapsText = document.getElementById('password-caps'); - const iconPasswordVisible = document.getElementById('visible-password'); - const iconPasswordInVisible = document.getElementById('invisible-password'); [usernameInput, passwordInput].forEach((input) => { input.addEventListener('keydown', (event) => { @@ -391,16 +391,11 @@ STUDIP.domReady(function () { event.preventDefault(); }); } +}); - document.getElementById('password-toggle').addEventListener('click', () => { - if (passwordInput.type === 'password') { - passwordInput.type = 'text'; - iconPasswordVisible.style.display = 'none'; - iconPasswordInVisible.style.display = ''; - } else { - passwordInput.type = 'password'; - iconPasswordVisible.style.display = ''; - iconPasswordInVisible.style.display = 'none'; - } - }); +/* Allow password inputs to display the password as plain text */ +STUDIP.ready(() => { + document.querySelectorAll('input[type="password"].allow-plaintext-toggle').forEach((node) => { + allowPlaintextToggle(node); + }) }); diff --git a/resources/assets/stylesheets/studip.scss b/resources/assets/stylesheets/studip.scss index 9183081..42ab072 100644 --- a/resources/assets/stylesheets/studip.scss +++ b/resources/assets/stylesheets/studip.scss @@ -627,3 +627,23 @@ mark { width: var(--icon-size-inline); height: var(--icon-size-inline); } + +/** Password plain toggle **/ +input.allow-plaintext-toggle { + padding-right: 28px !important; + + +.password-display-toggle { + @include hide-text; + @extend .studip-icon; + + cursor: pointer; + position: absolute; + transform: translateY(-50%); + + @include background-icon(visibility-invisible); + + &.password-is-hidden { + @include background-icon(visibility-checked); + } + } +} diff --git a/resources/vue/base-directives.js b/resources/vue/base-directives.js index 8f46b03..c7b8509 100644 --- a/resources/vue/base-directives.js +++ b/resources/vue/base-directives.js @@ -1,7 +1,9 @@ import autofocus from './directives/autofocus'; +import allowPlaintextToggle from "./directives/allow-plaintext-toggle"; const BaseDirectives = { autofocus, + allowPlaintextToggle, }; export default BaseDirectives; diff --git a/resources/vue/components/admission/PasswordAdmission.vue b/resources/vue/components/admission/PasswordAdmission.vue index 3f0ecf4..5d87e4d 100644 --- a/resources/vue/components/admission/PasswordAdmission.vue +++ b/resources/vue/components/admission/PasswordAdmission.vue @@ -12,15 +12,17 @@ @@ -43,14 +45,10 @@ export default { messageText: this.message || this.$gettext('Für die Anmeldung ist ein Passwort erforderlich.'), password1: '', password2: '', - passwordVisible: false, passwordSet: this.hasPassword } }, methods: { - togglePasswordVisible() { - this.passwordVisible = !this.passwordVisible; - }, setRuleData(data) { if (data.attributes.payload.password !== '') { this.passwordSet = true; diff --git a/resources/vue/directives/allow-plaintext-toggle.ts b/resources/vue/directives/allow-plaintext-toggle.ts new file mode 100644 index 0000000..7969c49 --- /dev/null +++ b/resources/vue/directives/allow-plaintext-toggle.ts @@ -0,0 +1,52 @@ +import {$gettext} from "../../assets/javascripts/lib/gettext"; +import {DirectiveBinding, nextTick} from "vue"; + +const messages = { + 0: $gettext('Passwort verstecken'), + 1: $gettext('Passwort anzeigen'), +}; + +function initialize(el: HTMLElement): void { + if (!el.parentElement) { + return; + } + + el.classList.add('allow-plaintext-toggle'); + + el.parentElement.style.position = 'relative'; + + const bbox = el.getBoundingClientRect(); + const parentBbox = el.parentElement.getBoundingClientRect(); + + const x = (bbox.x - parentBbox.x) + bbox.width - 26; + const y = (bbox.y - parentBbox.y) + bbox.height / 2; + + const toggle = document.createElement('button'); + toggle.classList.add('as-link', 'password-display-toggle', 'password-is-hidden'); + toggle.setAttribute('title', messages[1]); + toggle.innerText = messages[1]; + toggle.style.top = `${y}px`; + toggle.style.left = `${x}px`; + + toggle.addEventListener('click', (event) => { + const isHidden = el.getAttribute('type') === 'text'; + toggle.classList.toggle('password-is-hidden', isHidden); + toggle.setAttribute('title', messages[isHidden ? 1 : 0]); + toggle.innerText = messages[isHidden ? 1 : 0]; + + el.setAttribute('type', isHidden ? 'password' : 'text'); + + event.preventDefault(); + }); + + el.after(toggle); +} + +export default (el: HTMLElement, binding?: DirectiveBinding | undefined): void => { + if (el.nextElementSibling?.matches('button.password-display-toggle')) { + return; + } + + const promise = binding ? nextTick() : Promise.resolve(); + promise.then(() => initialize(el)) +} -- cgit v1.0