feat: move password to separate component
This commit is contained in:
parent
6041ad1482
commit
0322daf4d4
5 changed files with 98 additions and 86 deletions
85
src/components/input/password.vue
Normal file
85
src/components/input/password.vue
Normal file
|
@ -0,0 +1,85 @@
|
|||
<template>
|
||||
<div class="password-field">
|
||||
<input
|
||||
class="input"
|
||||
id="password"
|
||||
name="password"
|
||||
:placeholder="$t('user.auth.passwordPlaceholder')"
|
||||
required
|
||||
:type="passwordFieldType"
|
||||
autocomplete="current-password"
|
||||
@keyup.enter="e => $emit('submit', e)"
|
||||
:tabindex="props.tabindex"
|
||||
@focusout="validate"
|
||||
@input="handleInput"
|
||||
/>
|
||||
<a
|
||||
@click="togglePasswordFieldType"
|
||||
class="password-field-type-toggle"
|
||||
aria-label="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')"
|
||||
v-tooltip="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')">
|
||||
<icon :icon="passwordFieldType === 'password' ? 'eye' : 'eye-slash'"/>
|
||||
</a>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="!isValid">
|
||||
{{ $t('user.auth.passwordRequired') }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, watch} from "vue";
|
||||
import {useDebounceFn} from "@vueuse/core";
|
||||
|
||||
const props = defineProps({
|
||||
tabindex: String,
|
||||
modelValue: String,
|
||||
// This prop is a workaround to trigger validation from the outside when the user never had focus in the input.
|
||||
validateInitially: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['submit', 'update:modelValue'])
|
||||
|
||||
const passwordFieldType = ref<String>('password')
|
||||
const password = ref<String>('')
|
||||
const isValid = ref<Boolean>(!props.validateInitially)
|
||||
|
||||
watch(
|
||||
() => props.validateInitially,
|
||||
(doValidate: Boolean) => {
|
||||
if (doValidate) {
|
||||
validate()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function validate() {
|
||||
useDebounceFn(() => {
|
||||
isValid.value = password.value !== ''
|
||||
}, 100)()
|
||||
}
|
||||
|
||||
function togglePasswordFieldType() {
|
||||
passwordFieldType.value = passwordFieldType.value === 'password'
|
||||
? 'text'
|
||||
: 'password'
|
||||
}
|
||||
|
||||
function handleInput(e) {
|
||||
password.value = e.target.value
|
||||
emit('update:modelValue', e.target.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.password-field {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.password-field-type-toggle {
|
||||
position: absolute;
|
||||
color: var(--grey-400);
|
||||
top: 50%;
|
||||
right: 1rem;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
</style>
|
|
@ -4,4 +4,3 @@
|
|||
@import "task";
|
||||
@import "tasks";
|
||||
@import "namespaces";
|
||||
@import 'password-field-toggle';
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
.password-field-type-toggle {
|
||||
position: absolute;
|
||||
color: var(--grey-400);
|
||||
top: 50%;
|
||||
right: 1rem;
|
||||
transform: translateY(-50%);
|
||||
}
|
|
@ -39,31 +39,7 @@
|
|||
{{ $t('user.auth.forgotPassword') }}
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
id="password"
|
||||
name="password"
|
||||
:placeholder="$t('user.auth.passwordPlaceholder')"
|
||||
ref="password"
|
||||
required
|
||||
:type="passwordFieldType"
|
||||
autocomplete="current-password"
|
||||
@keyup.enter="submit"
|
||||
tabindex="2"
|
||||
@focusout="validateField('password')"
|
||||
/>
|
||||
<a
|
||||
@click="togglePasswordFieldType"
|
||||
class="password-field-type-toggle"
|
||||
aria-label="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')"
|
||||
v-tooltip="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')">
|
||||
<icon :icon="passwordFieldType === 'password' ? 'eye' : 'eye-slash'"/>
|
||||
</a>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="!passwordValid">
|
||||
{{ $t('user.auth.passwordRequired') }}
|
||||
</p>
|
||||
<password tabindex="2" @submit="submit" v-model="password" :validate-initially="validatePasswordInitially"/>
|
||||
</div>
|
||||
<div class="field" v-if="needsTotpPasscode">
|
||||
<label class="label" for="totpPasscode">{{ $t('user.auth.totpTitle') }}</label>
|
||||
|
@ -87,7 +63,6 @@
|
|||
@click="submit"
|
||||
:loading="loading"
|
||||
tabindex="4"
|
||||
:disabled="!allFieldsValid"
|
||||
>
|
||||
{{ $t('user.auth.login') }}
|
||||
</x-button>
|
||||
|
@ -129,9 +104,11 @@ import {getErrorText} from '@/message'
|
|||
import Message from '@/components/misc/message'
|
||||
import {redirectToProvider} from '../../helpers/redirectToProvider'
|
||||
import {getLastVisited, clearLastVisited} from '../../helpers/saveLastVisited'
|
||||
import Password from '@/components/input/password'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Password,
|
||||
Message,
|
||||
},
|
||||
data() {
|
||||
|
@ -139,8 +116,8 @@ export default {
|
|||
confirmedEmailSuccess: false,
|
||||
errorMessage: '',
|
||||
usernameValid: true,
|
||||
passwordValid: true,
|
||||
passwordFieldType: 'password',
|
||||
password: '',
|
||||
validatePasswordInitially: false,
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
|
@ -185,9 +162,6 @@ export default {
|
|||
this.openidConnect.providers &&
|
||||
this.openidConnect.providers.length > 0
|
||||
},
|
||||
allFieldsValid() {
|
||||
return this.usernameValid && this.passwordValid
|
||||
},
|
||||
...mapState({
|
||||
registrationEnabled: state => state.config.registrationEnabled,
|
||||
loading: LOADING,
|
||||
|
@ -215,12 +189,6 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
togglePasswordFieldType() {
|
||||
this.passwordFieldType = this.passwordFieldType === 'password'
|
||||
? 'text'
|
||||
: 'password'
|
||||
},
|
||||
|
||||
async submit() {
|
||||
this.errorMessage = ''
|
||||
// Some browsers prevent Vue bindings from working with autofilled values.
|
||||
|
@ -228,13 +196,13 @@ export default {
|
|||
// For more info, see https://kolaente.dev/vikunja/frontend/issues/78
|
||||
const credentials = {
|
||||
username: this.$refs.username.value,
|
||||
password: this.$refs.password.value,
|
||||
password: this.password,
|
||||
}
|
||||
|
||||
if (credentials.username === '' || credentials.password === '') {
|
||||
// Trigger the validation error messages
|
||||
this.validateField('username')
|
||||
this.validateField('password')
|
||||
this.validatePasswordInitially = true
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -46,30 +46,7 @@
|
|||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="password">{{ $t('user.auth.password') }}</label>
|
||||
<div class="control is-relative">
|
||||
<input
|
||||
class="input"
|
||||
id="password"
|
||||
name="password"
|
||||
:placeholder="$t('user.auth.passwordPlaceholder')"
|
||||
required
|
||||
:type="passwordFieldType"
|
||||
autocomplete="new-password"
|
||||
v-model="credentials.password"
|
||||
@keyup.enter="submit"
|
||||
@focusout="validatePassword"
|
||||
/>
|
||||
<a
|
||||
@click="togglePasswordFieldType"
|
||||
class="password-field-type-toggle"
|
||||
aria-label="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')"
|
||||
v-tooltip="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')">
|
||||
<icon :icon="passwordFieldType === 'password' ? 'eye' : 'eye-slash'"/>
|
||||
</a>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="!passwordValid">
|
||||
{{ $t('user.auth.passwordRequired') }}
|
||||
</p>
|
||||
<password @submit="submit" @update:modelValue="v => credentials.password = v" :validate-initially="validatePasswordInitially"/>
|
||||
</div>
|
||||
|
||||
<x-button
|
||||
|
@ -93,12 +70,13 @@
|
|||
|
||||
<script setup>
|
||||
import {useDebounceFn} from '@vueuse/core'
|
||||
import {ref, reactive, toRaw, computed, onBeforeMount} from 'vue'
|
||||
import {ref, reactive, toRaw, computed, onBeforeMount, watch} from 'vue'
|
||||
|
||||
import router from '@/router'
|
||||
import {store} from '@/store'
|
||||
import Message from '@/components/misc/message'
|
||||
import {isEmail} from '@/helpers/isEmail'
|
||||
import Password from '@/components/input/password'
|
||||
|
||||
// FIXME: use the `beforeEnter` hook of vue-router
|
||||
// Check if the user is already logged in, if so, redirect them to the homepage
|
||||
|
@ -116,6 +94,7 @@ const credentials = reactive({
|
|||
|
||||
const loading = computed(() => store.state.loading)
|
||||
const errorMessage = ref('')
|
||||
const validatePasswordInitially = ref(false)
|
||||
|
||||
const DEBOUNCE_TIME = 100
|
||||
|
||||
|
@ -130,29 +109,17 @@ const validateUsername = useDebounceFn(() => {
|
|||
usernameValid.value = credentials.username !== ''
|
||||
}, DEBOUNCE_TIME)
|
||||
|
||||
const passwordValid = ref(true)
|
||||
const validatePassword = useDebounceFn(() => {
|
||||
passwordValid.value = credentials.password !== ''
|
||||
}, DEBOUNCE_TIME)
|
||||
|
||||
const everythingValid = computed(() => {
|
||||
return credentials.username !== '' &&
|
||||
credentials.email !== '' &&
|
||||
credentials.password !== '' &&
|
||||
emailValid.value &&
|
||||
usernameValid.value &&
|
||||
passwordValid.value
|
||||
usernameValid.value
|
||||
})
|
||||
|
||||
const passwordFieldType = ref('password')
|
||||
const togglePasswordFieldType = () => {
|
||||
passwordFieldType.value = passwordFieldType.value === 'password'
|
||||
? 'text'
|
||||
: 'password'
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
errorMessage.value = ''
|
||||
validatePasswordInitially.value = true
|
||||
|
||||
if (!everythingValid.value) {
|
||||
return
|
||||
|
|
Loading…
Reference in a new issue