mirror of
https://github.com/Bubka/2FAuth.git
synced 2025-08-18 17:38:20 +02:00
Enable the Vue 3 front-end
This commit is contained in:
33
resources/js/components/formElements/Button.vue
Normal file
33
resources/js/components/formElements/Button.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
color: {
|
||||
type: String,
|
||||
default: 'is-link'
|
||||
},
|
||||
nativeType: {
|
||||
type: String,
|
||||
default: 'submit'
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isDisabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
:type="nativeType"
|
||||
:disabled="isLoading || isDisabled"
|
||||
:class="{
|
||||
'button': true,
|
||||
[`${color}`]: true,
|
||||
'is-loading': isLoading,
|
||||
}">
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
@@ -0,0 +1,89 @@
|
||||
<script setup>
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const mode = useColorMode()
|
||||
|
||||
const props = defineProps({
|
||||
returnTo: {
|
||||
type: Object,
|
||||
default: { name: 'accounts' }
|
||||
},
|
||||
action: {
|
||||
type: String,
|
||||
default: 'close'
|
||||
},
|
||||
useLinkTag: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
isText: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isCapture: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
})
|
||||
|
||||
const classes = 'button is-rounded'
|
||||
+ (mode.value === 'dark' && ! props.isText && ! props.isCapture ? ' is-dark' : '')
|
||||
+ (props.isText ? ' is-text' : '')
|
||||
+ (props.isCapture ? ' is-large is-warning' : '')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- back / close / cancel button -->
|
||||
<p v-if="useLinkTag" class="control">
|
||||
<RouterLink
|
||||
v-if="action == 'close'"
|
||||
id="btnClose"
|
||||
:to="returnTo"
|
||||
:class="classes"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
:aria-label="$t('commons.close_the_x_page', { pagetitle: $route.meta.title })"
|
||||
>
|
||||
{{ $t('commons.close') }}
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
v-else-if="action == 'back'"
|
||||
id="lnkBack"
|
||||
:to="returnTo"
|
||||
:class="classes"
|
||||
:aria-label="$t('commons.close_the_x_page', { pagetitle: $route.meta.title })"
|
||||
>
|
||||
{{ $t('commons.back') }}
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
v-else-if="action == 'cancel'"
|
||||
id="btnCancel"
|
||||
:to="returnTo"
|
||||
:class="classes"
|
||||
>
|
||||
{{ $t('commons.cancel') }}
|
||||
</RouterLink>
|
||||
</p>
|
||||
<p v-else class="control">
|
||||
<button
|
||||
v-if="action == 'close'"
|
||||
id="btnClose"
|
||||
:class="classes"
|
||||
@click="$emit('closed')"
|
||||
type="button"
|
||||
>
|
||||
{{ $t('commons.close') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="action == 'cancel'"
|
||||
id="btnCancel"
|
||||
:class="classes"
|
||||
@click="$emit('canceled')"
|
||||
type="button"
|
||||
>
|
||||
{{ $t('commons.cancel') }}
|
||||
</button>
|
||||
</p>
|
||||
</template>
|
20
resources/js/components/formElements/FieldError.vue
Normal file
20
resources/js/components/formElements/FieldError.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
error: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
field: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div role="alert">
|
||||
<p :id="'valError' + field[0].toUpperCase() + field.toLowerCase().slice(1)"
|
||||
class="help is-danger"
|
||||
v-html="error" />
|
||||
</div>
|
||||
</template>
|
329
resources/js/components/formElements/Form.js
vendored
Normal file
329
resources/js/components/formElements/Form.js
vendored
Normal file
@@ -0,0 +1,329 @@
|
||||
import { httpClientFactory } from '@/services/httpClientFactory'
|
||||
import Errors from './FormErrors'
|
||||
|
||||
class Form {
|
||||
/**
|
||||
* Create a new form instance.
|
||||
*
|
||||
* @param {Object} data
|
||||
*/
|
||||
constructor (data = {}) {
|
||||
this.axios = httpClientFactory('web')
|
||||
this.isBusy = false
|
||||
this.isDisabled = false
|
||||
// this.successful = false
|
||||
this.errors = new Errors()
|
||||
this.originalData = this.deepCopy(data)
|
||||
|
||||
Object.assign(this, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill form data.
|
||||
*
|
||||
* @param {Object} data
|
||||
*/
|
||||
fill (data) {
|
||||
this.keys().forEach(key => {
|
||||
this[key] = data[key]
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update original form data.
|
||||
*/
|
||||
setOriginal () {
|
||||
Object.keys(this)
|
||||
.filter(key => !Form.ignore.includes(key))
|
||||
.forEach(key => {
|
||||
this.originalData[key] = this.deepCopy(this[key])
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill form data.
|
||||
*
|
||||
* @param {Object} data
|
||||
*/
|
||||
hasChanged () {
|
||||
return this.keys().some(key => this[key] !== this.originalData[key])
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill form data.
|
||||
*
|
||||
* @param {Object} data
|
||||
*/
|
||||
fillWithKeyValueObject (data) {
|
||||
this.keys().forEach(key => {
|
||||
const keyValueObject = data.find(s => s.key === key.toString())
|
||||
if(keyValueObject != undefined) {
|
||||
this[key] = keyValueObject.value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the form data.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
data () {
|
||||
return this.keys().reduce((data, key) => (
|
||||
{ ...data, [key]: this[key] }
|
||||
), {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the form data keys.
|
||||
*
|
||||
* @return {Array}
|
||||
*/
|
||||
keys () {
|
||||
return Object.keys(this)
|
||||
.filter(key => !Form.ignore.includes(key))
|
||||
}
|
||||
|
||||
/**
|
||||
* Start processing the form.
|
||||
*/
|
||||
startProcessing () {
|
||||
this.errors.clear()
|
||||
this.isBusy = true
|
||||
// this.successful = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish processing the form.
|
||||
*/
|
||||
finishProcessing () {
|
||||
this.isBusy = false
|
||||
// this.successful = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the form errors.
|
||||
*/
|
||||
clear () {
|
||||
this.errors.clear()
|
||||
// this.successful = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the form fields.
|
||||
*/
|
||||
reset () {
|
||||
Object.keys(this)
|
||||
.filter(key => !Form.ignore.includes(key))
|
||||
.forEach(key => {
|
||||
this[key] = this.deepCopy(this.originalData[key])
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the form via a GET request.
|
||||
*
|
||||
* @param {String} url
|
||||
* @param {Object} config (axios config)
|
||||
* @return {Promise}
|
||||
*/
|
||||
get (url, config = {}) {
|
||||
return this.submit('get', url, config)
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the form via a POST request.
|
||||
*
|
||||
* @param {String} url
|
||||
* @param {Object} config (axios config)
|
||||
* @return {Promise}
|
||||
*/
|
||||
post (url, config = {}) {
|
||||
return this.submit('post', url, config)
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the form via a PATCH request.
|
||||
*
|
||||
* @param {String} url
|
||||
* @param {Object} config (axios config)
|
||||
* @return {Promise}
|
||||
*/
|
||||
patch (url, config = {}) {
|
||||
return this.submit('patch', url, config)
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the form via a PUT request.
|
||||
*
|
||||
* @param {String} url
|
||||
* @param {Object} config (axios config)
|
||||
* @return {Promise}
|
||||
*/
|
||||
put (url, config = {}) {
|
||||
return this.submit('put', url, config)
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the form via a DELETE request.
|
||||
*
|
||||
* @param {String} url
|
||||
* @param {Object} config (axios config)
|
||||
* @return {Promise}
|
||||
*/
|
||||
delete (url, config = {}) {
|
||||
return this.submit('delete', url, config)
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the form data via an HTTP request.
|
||||
*
|
||||
* @param {String} method (get, post, patch, put)
|
||||
* @param {String} url
|
||||
* @param {Object} config (axios config)
|
||||
* @return {Promise}
|
||||
*/
|
||||
submit (method, url, config = {}) {
|
||||
this.startProcessing()
|
||||
|
||||
const data = method === 'get'
|
||||
? { params: this.data() }
|
||||
: this.data()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// (Form.axios || axios).request({ url: this.route(url), method, data, ...config })
|
||||
this.axios.request({ url: this.route(url), method, data, ...config })
|
||||
.then(response => {
|
||||
this.finishProcessing()
|
||||
|
||||
resolve(response)
|
||||
})
|
||||
.catch(error => {
|
||||
this.isBusy = false
|
||||
|
||||
if (error.response) {
|
||||
this.errors.set(this.extractErrors(error.response))
|
||||
}
|
||||
if (error.response?.status != 422) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the form data via an HTTP request.
|
||||
*
|
||||
* @param {String} method (get, post, patch, put)
|
||||
* @param {String} url
|
||||
* @param {Object} config (axios config)
|
||||
* @return {Promise}
|
||||
*/
|
||||
upload (url, config = {}) {
|
||||
this.startProcessing()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// https://www.npmjs.com/package/axios#-automatic-serialization-to-formdata
|
||||
this.axios.post(this.route(url), this.data(), { headers: {'Content-Type' : 'multipart/form-data'}, ...config })
|
||||
.then(response => {
|
||||
this.finishProcessing()
|
||||
|
||||
resolve(response)
|
||||
})
|
||||
.catch(error => {
|
||||
this.isBusy = false
|
||||
|
||||
if (error.response) {
|
||||
this.errors.set(this.extractErrors(error.response))
|
||||
}
|
||||
// if (error.response?.status != 422) {
|
||||
reject(error)
|
||||
// }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the errors from the response object.
|
||||
*
|
||||
* @param {Object} response
|
||||
* @return {Object}
|
||||
*/
|
||||
extractErrors (response) {
|
||||
if (!response.data || typeof response.data !== 'object') {
|
||||
return { error: Form.errorMessage }
|
||||
}
|
||||
|
||||
if (response.data.errors) {
|
||||
return { ...response.data.errors }
|
||||
}
|
||||
|
||||
if (response.data.message) {
|
||||
return { error: response.data.message }
|
||||
}
|
||||
|
||||
return { ...response.data }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a named route.
|
||||
*
|
||||
* @param {String} name
|
||||
* @return {Object} parameters
|
||||
* @return {String}
|
||||
*/
|
||||
route (name, parameters = {}) {
|
||||
let url = name
|
||||
|
||||
if (Form.routes.hasOwnProperty(name)) {
|
||||
url = decodeURI(Form.routes[name])
|
||||
}
|
||||
|
||||
if (typeof parameters !== 'object') {
|
||||
parameters = { id: parameters }
|
||||
}
|
||||
|
||||
Object.keys(parameters).forEach(key => {
|
||||
url = url.replace(`{${key}}`, parameters[key])
|
||||
})
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear errors on keydown.
|
||||
*
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
onKeydown (event) {
|
||||
if (event.target.name) {
|
||||
this.errors.clear(event.target.name)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep copy the given object.
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @return {Object}
|
||||
*/
|
||||
deepCopy (obj) {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj
|
||||
}
|
||||
|
||||
const copy = Array.isArray(obj) ? [] : {}
|
||||
|
||||
Object.keys(obj).forEach(key => {
|
||||
copy[key] = this.deepCopy(obj[key])
|
||||
})
|
||||
|
||||
return copy
|
||||
}
|
||||
}
|
||||
|
||||
Form.routes = {}
|
||||
Form.errorMessage = 'Something went wrong. Please try again.'
|
||||
Form.ignore = ['isBusy', 'isDisabled', 'errors', 'originalData', 'axios']
|
||||
|
||||
export default Form
|
51
resources/js/components/formElements/FormButtons.vue
Normal file
51
resources/js/components/formElements/FormButtons.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
showCancelButton: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isBusy: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isDisabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
caption: {
|
||||
type: String,
|
||||
default: 'commons.submit'
|
||||
},
|
||||
cancelLandingView: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'is-link'
|
||||
},
|
||||
submitId: {
|
||||
type: String,
|
||||
default: 'btnSubmit'
|
||||
},
|
||||
cancelId: {
|
||||
type: String,
|
||||
default: 'btnCancel'
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<VueButton :id="submitId" :color="color" :isLoading="isBusy" :disabled="isDisabled" >
|
||||
{{ $t(caption) }}
|
||||
</VueButton>
|
||||
</div>
|
||||
<div class="control" v-if="showCancelButton">
|
||||
<RouterLink :id="cancelId" :to="{ name: cancelLandingView }" class="button is-text">
|
||||
{{ $t('commons.cancel') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
51
resources/js/components/formElements/FormCheckbox.vue
Normal file
51
resources/js/components/formElements/FormCheckbox.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script setup>
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: Boolean,
|
||||
fieldName: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: true
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
labelClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
help: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const attrs = useAttrs()
|
||||
const model = computed({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
emit("update:modelValue", value);
|
||||
},
|
||||
})
|
||||
|
||||
function toggleModel() {
|
||||
if (attrs['disabled'] != true) {
|
||||
model.value = !model.value
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="field">
|
||||
<input :id="fieldName" type="checkbox" :name="fieldName" class="is-checkradio is-info" v-model="model" v-bind="$attrs"/>
|
||||
<label tabindex="0" :for="fieldName" class="label" :class="labelClass" v-html="$t(label)" v-on:keypress.space.prevent="toggleModel" />
|
||||
<p class="help" v-html="$t(help)" v-if="help" />
|
||||
</div>
|
||||
</template>
|
141
resources/js/components/formElements/FormErrors.js
vendored
Normal file
141
resources/js/components/formElements/FormErrors.js
vendored
Normal file
@@ -0,0 +1,141 @@
|
||||
|
||||
export default class Errors {
|
||||
/**
|
||||
* Create a new error bag instance.
|
||||
*/
|
||||
constructor () {
|
||||
this.errors = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the errors object or field error messages.
|
||||
*
|
||||
* @param {Object|String} field
|
||||
* @param {Array|String|undefined} messages
|
||||
*/
|
||||
set (field, messages) {
|
||||
if (typeof field === 'object') {
|
||||
this.errors = field
|
||||
} else {
|
||||
this.set({ ...this.errors, [field]: arrayWrap(messages) })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the errors.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
all () {
|
||||
return this.errors
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if there is an error for the given field.
|
||||
*
|
||||
* @param {String} field
|
||||
* @return {Boolean}
|
||||
*/
|
||||
has (field) {
|
||||
return this.errors.hasOwnProperty(field)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if there are any errors for the given fields.
|
||||
*
|
||||
* @param {...String} fields
|
||||
* @return {Boolean}
|
||||
*/
|
||||
hasAny (...fields) {
|
||||
return fields.some(field => this.has(field))
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if there are any errors.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
any () {
|
||||
return Object.keys(this.errors).length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first error message for the given field.
|
||||
*
|
||||
* @param String} field
|
||||
* @return {String|undefined}
|
||||
*/
|
||||
get (field) {
|
||||
if (this.has(field)) {
|
||||
return this.getAll(field)[0]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the error messages for the given field.
|
||||
*
|
||||
* @param {String} field
|
||||
* @return {Array}
|
||||
*/
|
||||
getAll (field) {
|
||||
return arrayWrap(this.errors[field] || [])
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error message for the given fields.
|
||||
*
|
||||
* @param {...String} fields
|
||||
* @return {Array}
|
||||
*/
|
||||
only (...fields) {
|
||||
const messages = []
|
||||
|
||||
fields.forEach(field => {
|
||||
const message = this.get(field)
|
||||
|
||||
if (message) {
|
||||
messages.push(message)
|
||||
}
|
||||
})
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the errors in a flat array.
|
||||
*
|
||||
* @return {Array}
|
||||
*/
|
||||
flatten () {
|
||||
return Object.values(this.errors).reduce((a, b) => a.concat(b), [])
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear one or all error fields.
|
||||
*
|
||||
* @param {String|undefined} field
|
||||
*/
|
||||
clear (field) {
|
||||
const errors = {}
|
||||
|
||||
if (field) {
|
||||
Object.keys(this.errors).forEach(key => {
|
||||
if (key !== field) {
|
||||
errors[key] = this.errors[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.set(errors)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the given value is not an array, wrap it in one.
|
||||
*
|
||||
* @param {Any} value
|
||||
* @return {Array}
|
||||
*/
|
||||
function arrayWrap (value) {
|
||||
return Array.isArray(value) ? value : [value]
|
||||
}
|
68
resources/js/components/formElements/FormField.vue
Normal file
68
resources/js/components/formElements/FormField.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<script setup>
|
||||
import { useIdGenerator } from '@/composables/helpers'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const { inputId } = useIdGenerator(props.inputType, props.fieldName)
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: [String, Number, Boolean],
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
fieldName: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: true
|
||||
},
|
||||
fieldError: [String],
|
||||
inputType: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
help: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
hasOffset: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isDisabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
maxLength: {
|
||||
type: Number,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="field" :class="{ 'pt-3' : hasOffset }">
|
||||
<label :for="inputId" class="label" v-html="$t(label)"></label>
|
||||
<div class="control">
|
||||
<input
|
||||
:disabled="isDisabled"
|
||||
:id="inputId"
|
||||
:type="inputType"
|
||||
class="input"
|
||||
:value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
v-bind="$attrs"
|
||||
v-on:change="$emit('update:modelValue', $event.target.value)"
|
||||
:maxlength="maxLength"
|
||||
/>
|
||||
</div>
|
||||
<FieldError v-if="fieldError != undefined" :error="fieldError" :field="fieldName" />
|
||||
<p class="help" v-html="$t(help)" v-if="help"></p>
|
||||
</div>
|
||||
</template>
|
97
resources/js/components/formElements/FormLockField.vue
Normal file
97
resources/js/components/formElements/FormLockField.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<script setup>
|
||||
import { useIdGenerator } from '@/composables/helpers'
|
||||
import { UseColorMode } from '@vueuse/components'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const { inputId } = useIdGenerator(props.inputType, props.fieldName)
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: [String, Number, Boolean],
|
||||
isEditMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
fieldName: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: true
|
||||
},
|
||||
fieldError: [String],
|
||||
inputType: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
help: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
hasOffset: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isDisabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isExpanded: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
maxLength: {
|
||||
type: Number,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const fieldIsLocked = ref(props.isDisabled || props.isEditMode)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="field" style="margin-bottom: 0.5rem;">
|
||||
<label :for="inputId" class="label" v-html="$t(label)" />
|
||||
</div>
|
||||
<div class="field has-addons" :class="{ 'pt-3' : hasOffset }">
|
||||
<div class="control" :class="{ 'is-expanded': isExpanded }">
|
||||
<input
|
||||
:disabled="fieldIsLocked"
|
||||
:id="inputId"
|
||||
:type="inputType"
|
||||
class="input"
|
||||
:value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
v-bind="$attrs"
|
||||
v-on:change="$emit('update:modelValue', $event.target.value)"
|
||||
:maxlength="maxLength"
|
||||
/>
|
||||
</div>
|
||||
<UseColorMode v-slot="{ mode }" v-if="isEditMode">
|
||||
<div class="control" v-if="fieldIsLocked">
|
||||
<button type="button" class="button field-lock" :class="{'is-dark' : mode == 'dark'}" @click.stop="fieldIsLocked = false" :title="$t('twofaccounts.forms.unlock.title')">
|
||||
<span class="icon">
|
||||
<FontAwesomeIcon :icon="['fas', 'lock']" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control" v-else>
|
||||
<button type="button" class="button field-unlock" :class="{'is-dark' : mode == 'dark'}" @click.stop="fieldIsLocked = true" :title="$t('twofaccounts.forms.lock.title')">
|
||||
<span class="icon has-text-danger">
|
||||
<FontAwesomeIcon :icon="['fas', 'lock-open']" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</UseColorMode>
|
||||
</div>
|
||||
<FieldError v-if="fieldError != undefined" :error="fieldError" :field="fieldName" />
|
||||
<p class="help" v-html="$t(help)" v-if="help"></p>
|
||||
</template>
|
116
resources/js/components/formElements/FormPasswordField.vue
Normal file
116
resources/js/components/formElements/FormPasswordField.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<script setup>
|
||||
import { useIdGenerator } from '@/composables/helpers'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: true
|
||||
})
|
||||
|
||||
const { inputId } = useIdGenerator(props.inputType, props.fieldName)
|
||||
const currentType = ref(props.inputType)
|
||||
const hasCapsLockOn = ref(false)
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: [String],
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
fieldName: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: true
|
||||
},
|
||||
fieldError: [String],
|
||||
inputType: {
|
||||
type: String,
|
||||
default: 'password'
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
help: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
hasOffset: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isDisabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showRules: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
})
|
||||
|
||||
const hasLowerCase = computed(() => {
|
||||
return /[a-z]/.test(props.modelValue)
|
||||
})
|
||||
const hasUpperCase = computed(() => {
|
||||
return /[A-Z]/.test(props.modelValue)
|
||||
})
|
||||
const hasNumber = computed(() => {
|
||||
return /[0-9]/.test(props.modelValue)
|
||||
})
|
||||
const hasSpecialChar = computed(() => {
|
||||
return /[^A-Za-z0-9]/.test(props.modelValue)
|
||||
})
|
||||
const IsLongEnough = computed(() => {
|
||||
return props.modelValue.length >= 8
|
||||
})
|
||||
|
||||
function checkCapsLock(event) {
|
||||
hasCapsLockOn.value = event.getModifierState('CapsLock') ? true : false
|
||||
}
|
||||
|
||||
function setFieldType(event) {
|
||||
if (currentType.value != event) {
|
||||
currentType.value = event
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="field" :class="{ 'pt-3' : hasOffset }">
|
||||
<label :for="inputId" class="label" v-html="$t(label)" />
|
||||
<div class="control has-icons-right">
|
||||
<input
|
||||
:disabled="isDisabled"
|
||||
:id="inputId"
|
||||
:type="currentType"
|
||||
class="input"
|
||||
:value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
v-bind="$attrs"
|
||||
v-on:input="$emit('update:modelValue', $event.target.value)"
|
||||
v-on:keyup="checkCapsLock"
|
||||
/>
|
||||
<span v-if="currentType == 'password'" role="button" id="btnTogglePassword" tabindex="0" class="icon is-small is-right is-clickable" @keyup.enter="setFieldType('text')" @click="setFieldType('text')" :title="$t('auth.forms.reveal_password')">
|
||||
<font-awesome-icon :icon="['fas', 'eye-slash']" />
|
||||
</span>
|
||||
<span v-else role="button" id="btnTogglePassword" tabindex="0" class="icon is-small is-right is-clickable" @keyup.enter="setFieldType('password')" @click="setFieldType('password')" :title="$t('auth.forms.hide_password')">
|
||||
<font-awesome-icon :icon="['fas', 'eye']" />
|
||||
</span>
|
||||
</div>
|
||||
<p class="help is-warning" v-if="hasCapsLockOn" v-html="$t('auth.forms.caps_lock_is_on')" />
|
||||
<FieldError v-if="fieldError != undefined" :error="fieldError" :field="fieldName" />
|
||||
<p class="help" v-html="$t(help)" v-if="help" />
|
||||
<div v-if="showRules" class="columns is-mobile is-size-7 mt-0">
|
||||
<div class="column is-one-third">
|
||||
<span class="has-text-weight-semibold">{{ $t("auth.forms.mandatory_rules") }}</span><br />
|
||||
<span class="is-underscored" id="valPwdIsLongEnough" :class="{'is-dot' : IsLongEnough}"></span>{{ $t('auth.forms.is_long_enough') }}<br/>
|
||||
</div>
|
||||
<div class="column">
|
||||
<span class="has-text-weight-semibold">{{ $t("auth.forms.optional_rules_you_should_follow") }}</span><br />
|
||||
<span class="is-underscored" id="valPwdHasLowerCase" :class="{'is-dot' : hasLowerCase}"></span>{{ $t('auth.forms.has_lower_case') }}<br/>
|
||||
<span class="is-underscored" id="valPwdHasUpperCase" :class="{'is-dot' : hasUpperCase}"></span>{{ $t('auth.forms.has_upper_case') }}<br/>
|
||||
<span class="is-underscored" id="valPwdHasSpecialChar" :class="{'is-dot' : hasSpecialChar}"></span>{{ $t('auth.forms.has_special_char') }}<br/>
|
||||
<span class="is-underscored" id="valPwdHasNumber" :class="{'is-dot' : hasNumber}"></span>{{ $t('auth.forms.has_number') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
40
resources/js/components/formElements/FormSelect.vue
Normal file
40
resources/js/components/formElements/FormSelect.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
modelValue: [String, Number, Boolean],
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
fieldName: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: true
|
||||
},
|
||||
fieldError: [String],
|
||||
options: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
help: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
})
|
||||
|
||||
const selected = ref(props.modelValue)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="field">
|
||||
<label class="label" v-html="$t(label)"></label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select v-model="selected" v-on:change="$emit('update:modelValue', $event.target.value)">
|
||||
<option v-for="option in options" :value="option.value">{{ $t(option.text) }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<FieldError v-if="fieldError != undefined" :error="fieldError" :field="fieldName" />
|
||||
<p class="help" v-html="$t(help)" v-if="help"></p>
|
||||
</div>
|
||||
</template>
|
74
resources/js/components/formElements/FormToggle.vue
Normal file
74
resources/js/components/formElements/FormToggle.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<script setup>
|
||||
import { useIdGenerator } from '@/composables/helpers'
|
||||
import { UseColorMode } from '@vueuse/components'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: [String, Number, Boolean],
|
||||
choices: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
fieldName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
fieldError: [String],
|
||||
hasOffset: Boolean,
|
||||
isDisabled: Boolean,
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
help: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
})
|
||||
|
||||
// defines what events our component emits
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
function setRadio(event) {
|
||||
emit('update:modelValue', event)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="field" :class="{ 'pt-3': hasOffset }" role="radiogroup"
|
||||
:aria-labelledby="useIdGenerator('label',fieldName).inputId">
|
||||
<label v-if="label" :id="useIdGenerator('label',fieldName).inputId" class="label" v-html="$t(label)" />
|
||||
<div class="is-toggle buttons">
|
||||
<UseColorMode v-slot="{ mode }">
|
||||
<button
|
||||
v-for="choice in choices"
|
||||
:key="choice.value"
|
||||
:id="useIdGenerator('button',fieldName+choice.value).inputId"
|
||||
role="radio"
|
||||
type="button"
|
||||
class="button"
|
||||
:aria-checked="modelValue===choice.value"
|
||||
:disabled="isDisabled"
|
||||
:class="{
|
||||
'is-link': modelValue===choice.value,
|
||||
'is-dark': mode==='dark',
|
||||
'is-multiline': choice.legend,
|
||||
}"
|
||||
v-on:click.stop="setRadio(choice.value)"
|
||||
:title="choice.title? choice.title:''">
|
||||
<input
|
||||
:id="useIdGenerator('radio',choice.value).inputId"
|
||||
type="radio"
|
||||
class="is-hidden"
|
||||
:checked="modelValue===choice.value"
|
||||
:value="choice.value"
|
||||
:disabled="isDisabled" />
|
||||
<span v-if="choice.legend" v-html="$t(choice.legend)" class="is-block is-size-7" />
|
||||
<FontAwesomeIcon :icon="['fas',choice.icon]" v-if="choice.icon" class="mr-2" /> {{ $t(choice.text) }}
|
||||
</button>
|
||||
</UseColorMode>
|
||||
</div>
|
||||
<FieldError v-if="fieldError != undefined" :error="fieldError" :field="fieldName" />
|
||||
<p class="help" v-html="$t(help)" v-if="help" />
|
||||
</div>
|
||||
</template>
|
Reference in New Issue
Block a user