@dargmuesli/nuxt-vio 2.0.1 → 3.0.0-beta.1
Sign up to get free protection for your applications and to get access to all the features.
- package/app.config.ts +78 -34
- package/components/{VioApp.vue → _/VioApp.vue} +23 -5
- package/components/button/VioButtonColored.vue +52 -0
- package/components/card/VioCard.vue +19 -0
- package/components/card/state/VioCardState.vue +20 -0
- package/components/card/state/VioCardStateAlert.vue +14 -0
- package/components/form/VioForm.vue +84 -0
- package/components/form/VioFormCheckbox.vue +27 -0
- package/components/form/input/VioFormInput.vue +192 -0
- package/components/form/input/VioFormInputIconWrapper.vue +7 -0
- package/components/form/input/VioFormInputUrl.vue +54 -0
- package/components/form/input/state/VioFormInputState.vue +5 -0
- package/components/form/input/state/VioFormInputStateError.vue +32 -0
- package/components/form/input/state/VioFormInputStateInfo.vue +32 -0
- package/components/icon/IconArrowRight.vue +31 -0
- package/components/icon/IconCalendar.vue +31 -0
- package/components/icon/IconChatOutline.vue +27 -0
- package/components/icon/IconChatSolid.vue +26 -0
- package/components/icon/IconCheckCircle.vue +29 -0
- package/components/icon/IconContainer.vue +15 -0
- package/components/icon/IconDownload.vue +31 -0
- package/components/icon/IconExclamationCircle.vue +29 -0
- package/components/icon/IconHome.vue +31 -0
- package/components/icon/IconHourglass.vue +32 -0
- package/components/icon/IconLightbulb.vue +27 -0
- package/components/icon/IconLogo.vue +17 -0
- package/components/icon/IconMixcloud.vue +23 -0
- package/components/icon/IconMusic.vue +27 -0
- package/components/icon/IconPlay.vue +25 -0
- package/components/icon/IconShare.vue +27 -0
- package/components/layout/VioLayoutBreadcrumbs.vue +83 -0
- package/components/layout/VioLayoutFooter.vue +40 -0
- package/components/layout/VioLayoutFooterCategory.vue +17 -0
- package/components/layout/VioLayoutHeader.vue +98 -0
- package/components/layout/VioLayoutSpanList.vue +20 -0
- package/components/loader/indicator/VioLoaderIndicator.vue +14 -0
- package/components/loader/indicator/VioLoaderIndicatorPing.vue +12 -0
- package/components/loader/indicator/VioLoaderIndicatorSpinner.vue +24 -0
- package/components/{VioLegalNotice.vue → page/VioPageLegalNotice.vue} +1 -1
- package/components/{VioPrivacyPolicy.vue → page/VioPagePrivacyPolicy.vue} +1 -1
- package/composables/useAppLayout.ts +2 -2
- package/composables/useDateTime.ts +17 -0
- package/composables/useFavicons.ts +2 -2
- package/composables/useFireError.ts +17 -0
- package/composables/useGetServiceHref.ts +47 -0
- package/composables/useHeadDefault.ts +34 -0
- package/composables/useHeadLayout.ts +67 -0
- package/composables/useStrapiFetch.ts +10 -0
- package/error.vue +3 -2
- package/locales/de.json +7 -1
- package/locales/en.json +7 -1
- package/nuxt.config.ts +38 -2
- package/package.json +24 -10
- package/pages/legal-notice.vue +1 -1
- package/pages/privacy-policy.vue +1 -1
- package/plugins/dayjs.ts +46 -0
- package/plugins/i18n.ts +5 -0
- package/plugins/marked.ts +9 -0
- package/server/middleware/headers.ts +41 -10
- package/tailwind.config.ts +131 -8
- package/utils/constants.ts +9 -1
- package/utils/form.ts +19 -0
- package/utils/networking.ts +82 -0
- package/utils/text.ts +20 -0
- /package/components/{VioError.vue → _/VioError.vue} +0 -0
- /package/components/{VioLink.vue → _/VioLink.vue} +0 -0
- /package/components/{VioButton.vue → button/VioButton.vue} +0 -0
- /package/components/{VioLayout.vue → layout/VioLayout.vue} +0 -0
- /package/components/{VioHr.vue → layout/VioLayoutHr.vue} +0 -0
package/app.config.ts
CHANGED
@@ -1,57 +1,101 @@
|
|
1
|
-
import {
|
1
|
+
import { useServerSeoMeta } from '@unhead/vue'
|
2
2
|
|
3
3
|
export default defineAppConfig({
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
vio: {
|
5
|
+
legalNotice: undefined,
|
6
|
+
privacyPolicy: undefined,
|
7
|
+
seoMeta: {
|
8
|
+
twitterSite: '@dargmuesli',
|
9
|
+
},
|
10
|
+
server: {
|
11
|
+
middleware: {
|
12
|
+
headers: {
|
13
|
+
csp: {
|
14
|
+
default: {
|
15
|
+
'Cross-Origin-Opener-Policy': 'same-origin',
|
16
|
+
// 'Cross-Origin-Embedder-Policy', 'require-corp') // https://stackoverflow.com/questions/71904052/getting-notsameoriginafterdefaultedtosameoriginbycoep-error-with-helmet
|
17
|
+
'Cross-Origin-Resource-Policy': 'same-origin',
|
18
|
+
// 'Expect-CT', 'max-age=0') // deprecated (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect-CT)
|
19
|
+
NEL: '\'{"report_to":"default","max_age":31536000,"include_subdomains":true}\'',
|
20
|
+
'Origin-Agent-Cluster': '?1',
|
21
|
+
'Permissions-Policy': '',
|
22
|
+
'Referrer-Policy': 'no-referrer',
|
23
|
+
'Report-To':
|
24
|
+
'\'{"group":"default":"max_age":31536000:"endpoints":[{"url":"https://dargmuesli.report-uri.com/a/d/g"}]:"include_subdomains":true}\'',
|
25
|
+
'X-Content-Type-Options': 'nosniff',
|
26
|
+
'X-DNS-Prefetch-Control': 'off',
|
27
|
+
'X-Download-Options': 'noopen',
|
28
|
+
'X-Frame-Options': 'SAMEORIGIN',
|
29
|
+
'X-Permitted-Cross-Domain-Policies': 'none',
|
30
|
+
'X-XSS-Protection': '1; mode=block', // TODO: set back to `0` once CSP does not use `unsafe-*` anymore (https://github.com/maevsi/maevsi/issues/1047)
|
31
|
+
},
|
32
|
+
production: {
|
33
|
+
'Strict-Transport-Security':
|
34
|
+
'max-age=31536000; includeSubDomains; preload',
|
35
|
+
},
|
36
|
+
},
|
37
|
+
},
|
38
|
+
},
|
39
|
+
},
|
40
|
+
themeColor: '#202020',
|
8
41
|
},
|
9
|
-
themeColor: '#202020',
|
10
42
|
})
|
11
43
|
|
12
44
|
declare module 'nuxt/schema' {
|
13
|
-
interface
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
responsibility: {
|
19
|
-
address: {
|
20
|
-
city: string
|
21
|
-
name: string
|
22
|
-
street: string
|
23
|
-
}
|
24
|
-
}
|
25
|
-
tmg: {
|
26
|
-
address: {
|
27
|
-
city: string
|
28
|
-
name: string
|
29
|
-
street: string
|
45
|
+
interface AppConfig {
|
46
|
+
vio: {
|
47
|
+
legalNotice?: {
|
48
|
+
contact: {
|
49
|
+
email: string
|
30
50
|
}
|
31
|
-
|
32
|
-
}
|
33
|
-
privacyPolicy?: {
|
34
|
-
hostingCdn?: {
|
35
|
-
external: {
|
51
|
+
responsibility: {
|
36
52
|
address: {
|
37
53
|
city: string
|
38
54
|
name: string
|
39
55
|
street: string
|
40
56
|
}
|
41
57
|
}
|
42
|
-
|
43
|
-
mandatoryInfo?: {
|
44
|
-
responsible: {
|
58
|
+
tmg: {
|
45
59
|
address: {
|
46
60
|
city: string
|
47
|
-
email: string
|
48
61
|
name: string
|
49
62
|
street: string
|
50
63
|
}
|
51
64
|
}
|
52
65
|
}
|
66
|
+
privacyPolicy?: {
|
67
|
+
hostingCdn?: {
|
68
|
+
external: {
|
69
|
+
address: {
|
70
|
+
city: string
|
71
|
+
name: string
|
72
|
+
street: string
|
73
|
+
}
|
74
|
+
}
|
75
|
+
}
|
76
|
+
mandatoryInfo?: {
|
77
|
+
responsible: {
|
78
|
+
address: {
|
79
|
+
city: string
|
80
|
+
email: string
|
81
|
+
name: string
|
82
|
+
street: string
|
83
|
+
}
|
84
|
+
}
|
85
|
+
}
|
86
|
+
}
|
87
|
+
seoMeta?: Parameters<typeof useServerSeoMeta>[0]
|
88
|
+
server: {
|
89
|
+
middleware: {
|
90
|
+
headers: {
|
91
|
+
csp: {
|
92
|
+
default: Record<string, string>
|
93
|
+
production: Record<string, string>
|
94
|
+
}
|
95
|
+
}
|
96
|
+
}
|
97
|
+
}
|
98
|
+
themeColor?: string
|
53
99
|
}
|
54
|
-
seoMeta?: Parameters<typeof useSeoMeta>[0]
|
55
|
-
themeColor?: string
|
56
100
|
}
|
57
101
|
}
|
@@ -14,19 +14,36 @@
|
|
14
14
|
import { Locale } from '@dargmuesli/nuxt-cookie-control/dist/runtime/types'
|
15
15
|
|
16
16
|
export interface Props {
|
17
|
-
siteDescription: string
|
18
17
|
ogImageAlt: string
|
19
18
|
ogImageComponent?: string
|
19
|
+
siteDescription: string
|
20
20
|
}
|
21
21
|
const props = withDefaults(defineProps<Props>(), {
|
22
22
|
ogImageComponent: undefined,
|
23
23
|
})
|
24
|
+
const ogImageAltProp = toRef(() => props.ogImageAlt)
|
25
|
+
const ogImageComponentProp = toRef(() => props.ogImageComponent)
|
26
|
+
const siteDescriptionProp = toRef(() => props.siteDescription)
|
24
27
|
|
25
28
|
const { locale } = useI18n()
|
26
29
|
const cookieControl = useCookieControl()
|
27
30
|
|
28
31
|
const { loadingIds, indicateLoadingDone } = useLoadingDoneIndicator('app')
|
29
32
|
|
33
|
+
// methods
|
34
|
+
const init = () => {
|
35
|
+
if (process.client) {
|
36
|
+
const cookieTimezone = useCookie(TIMEZONE_COOKIE_NAME, {
|
37
|
+
// default: () => undefined, // setting `default` on the client side only does not write the cookie
|
38
|
+
httpOnly: false,
|
39
|
+
sameSite: 'strict',
|
40
|
+
secure: true,
|
41
|
+
})
|
42
|
+
// @ts-ignore `tz` should be part of `$dayjs` (https://github.com/iamkun/dayjs/issues/2106)
|
43
|
+
cookieTimezone.value = $dayjs.tz.guess()
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
30
47
|
// computations
|
31
48
|
const isLoading = computed(() => !!loadingIds.value.length)
|
32
49
|
|
@@ -46,13 +63,14 @@ watch(
|
|
46
63
|
)
|
47
64
|
|
48
65
|
// initialization
|
66
|
+
init()
|
49
67
|
updateSiteConfig({
|
50
|
-
description:
|
68
|
+
description: siteDescriptionProp.value,
|
51
69
|
})
|
52
70
|
defineOgImage({
|
53
|
-
alt:
|
54
|
-
component:
|
55
|
-
description:
|
71
|
+
alt: ogImageAltProp.value,
|
72
|
+
component: ogImageComponentProp.value,
|
73
|
+
description: siteDescriptionProp.value,
|
56
74
|
})
|
57
75
|
useAppLayout()
|
58
76
|
useFavicons()
|
@@ -0,0 +1,52 @@
|
|
1
|
+
<template>
|
2
|
+
<VioButton
|
3
|
+
:is-to-relative="isToRelative"
|
4
|
+
:aria-label="ariaLabel"
|
5
|
+
class="rounded-md border px-4 py-2 font-medium"
|
6
|
+
:class="
|
7
|
+
[
|
8
|
+
...(isPrimary
|
9
|
+
? [
|
10
|
+
'border-transparent bg-gray-800 text-text-bright hover:bg-black dark:bg-yellow-500 dark:text-gray-800 dark:hover:bg-yellow-600',
|
11
|
+
]
|
12
|
+
: [
|
13
|
+
'border-gray-300 text-text-dark hover:bg-black/5 dark:border-gray-600 dark:text-text-bright dark:hover:bg-black/30',
|
14
|
+
]),
|
15
|
+
].join(' ')
|
16
|
+
"
|
17
|
+
:disabled="disabled"
|
18
|
+
:to="to"
|
19
|
+
:type="type"
|
20
|
+
@click="emit('click')"
|
21
|
+
>
|
22
|
+
<slot />
|
23
|
+
<template #prefix>
|
24
|
+
<slot name="prefix" />
|
25
|
+
</template>
|
26
|
+
<template #suffix>
|
27
|
+
<slot name="suffix" />
|
28
|
+
</template>
|
29
|
+
</VioButton>
|
30
|
+
</template>
|
31
|
+
|
32
|
+
<script setup lang="ts">
|
33
|
+
export interface Props {
|
34
|
+
ariaLabel: string
|
35
|
+
disabled?: boolean
|
36
|
+
isPrimary?: boolean
|
37
|
+
isToRelative?: boolean
|
38
|
+
to?: string
|
39
|
+
type?: 'button' | 'reset' | 'submit'
|
40
|
+
}
|
41
|
+
withDefaults(defineProps<Props>(), {
|
42
|
+
disabled: false,
|
43
|
+
isPrimary: true,
|
44
|
+
isToRelative: false,
|
45
|
+
to: undefined,
|
46
|
+
type: 'button',
|
47
|
+
})
|
48
|
+
|
49
|
+
const emit = defineEmits<{
|
50
|
+
click: []
|
51
|
+
}>()
|
52
|
+
</script>
|
@@ -0,0 +1,19 @@
|
|
1
|
+
<template>
|
2
|
+
<div
|
3
|
+
class="overflow-hidden rounded-lg"
|
4
|
+
:class="[backgroundColor, ...(isHigh ? ['px-4 py-6'] : ['p-4'])]"
|
5
|
+
>
|
6
|
+
<slot />
|
7
|
+
</div>
|
8
|
+
</template>
|
9
|
+
|
10
|
+
<script setup lang="ts">
|
11
|
+
export interface Props {
|
12
|
+
backgroundColor?: string
|
13
|
+
isHigh?: boolean
|
14
|
+
}
|
15
|
+
withDefaults(defineProps<Props>(), {
|
16
|
+
backgroundColor: 'bg-background-brighten dark:bg-background-darken',
|
17
|
+
isHigh: false,
|
18
|
+
})
|
19
|
+
</script>
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<template>
|
2
|
+
<VioCard
|
3
|
+
:background-color="backgroundColor"
|
4
|
+
class="border-0 text-center font-medium text-white"
|
5
|
+
:class="{ 'rounded-none': isEdgy }"
|
6
|
+
>
|
7
|
+
<slot />
|
8
|
+
</VioCard>
|
9
|
+
</template>
|
10
|
+
|
11
|
+
<script setup lang="ts">
|
12
|
+
export interface Props {
|
13
|
+
backgroundColor?: string
|
14
|
+
isEdgy?: boolean
|
15
|
+
}
|
16
|
+
withDefaults(defineProps<Props>(), {
|
17
|
+
backgroundColor: undefined,
|
18
|
+
isEdgy: false,
|
19
|
+
})
|
20
|
+
</script>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<template>
|
2
|
+
<VioCardState background-color="bg-red-600" :is-edgy="isEdgy" role="alert">
|
3
|
+
<slot />
|
4
|
+
</VioCardState>
|
5
|
+
</template>
|
6
|
+
|
7
|
+
<script setup lang="ts">
|
8
|
+
export interface Props {
|
9
|
+
isEdgy?: boolean
|
10
|
+
}
|
11
|
+
withDefaults(defineProps<Props>(), {
|
12
|
+
isEdgy: false,
|
13
|
+
})
|
14
|
+
</script>
|
@@ -0,0 +1,84 @@
|
|
1
|
+
<template>
|
2
|
+
<form
|
3
|
+
v-if="form"
|
4
|
+
:class="[
|
5
|
+
{ 'animate-shake rounded-lg border border-red-500': errors?.length },
|
6
|
+
formClass,
|
7
|
+
]"
|
8
|
+
novalidate
|
9
|
+
@submit="(e) => emit('submit', e)"
|
10
|
+
>
|
11
|
+
<VioCard class="flex flex-col" is-high>
|
12
|
+
<div class="flex flex-col min-h-0 overflow-y-auto gap-6">
|
13
|
+
<slot />
|
14
|
+
<div class="flex flex-col items-center justify-between">
|
15
|
+
<VioButtonColored
|
16
|
+
:aria-label="submitName || t('submit')"
|
17
|
+
:class="{
|
18
|
+
'animate-shake': form.$error,
|
19
|
+
}"
|
20
|
+
type="submit"
|
21
|
+
@click="emit('click')"
|
22
|
+
>
|
23
|
+
{{ submitName || t('submit') }}
|
24
|
+
<template #prefix>
|
25
|
+
<slot name="submit-icon" />
|
26
|
+
</template>
|
27
|
+
</VioButtonColored>
|
28
|
+
<VioFormInputStateError v-if="form.$error" class="mt-2">
|
29
|
+
{{ t('globalValidationFailed') }}
|
30
|
+
</VioFormInputStateError>
|
31
|
+
</div>
|
32
|
+
<VioCardStateAlert v-if="errorMessages?.length" class="my-4">
|
33
|
+
<VioLayoutSpanList :span="errorMessages" />
|
34
|
+
</VioCardStateAlert>
|
35
|
+
<div v-if="$slots.assistance" class="flex justify-center">
|
36
|
+
<slot name="assistance" />
|
37
|
+
</div>
|
38
|
+
</div>
|
39
|
+
</VioCard>
|
40
|
+
</form>
|
41
|
+
</template>
|
42
|
+
|
43
|
+
<script setup lang="ts">
|
44
|
+
import type { BaseValidation } from '@vuelidate/core'
|
45
|
+
|
46
|
+
import type { BackendError } from '~/../types/api'
|
47
|
+
|
48
|
+
export interface Props {
|
49
|
+
errors?: BackendError[]
|
50
|
+
errorsPgIds?: Record<string, string>
|
51
|
+
form: BaseValidation
|
52
|
+
formClass?: string
|
53
|
+
isFormSent?: boolean
|
54
|
+
submitName?: string
|
55
|
+
}
|
56
|
+
const props = withDefaults(defineProps<Props>(), {
|
57
|
+
errors: undefined,
|
58
|
+
errorsPgIds: undefined,
|
59
|
+
formClass: undefined,
|
60
|
+
isFormSent: false,
|
61
|
+
submitName: undefined,
|
62
|
+
})
|
63
|
+
|
64
|
+
const emit = defineEmits<{
|
65
|
+
click: []
|
66
|
+
submit: [event: Event]
|
67
|
+
}>()
|
68
|
+
|
69
|
+
const { t } = useI18n()
|
70
|
+
|
71
|
+
// computations
|
72
|
+
const errorMessages = computed(() =>
|
73
|
+
props.errors
|
74
|
+
? getCombinedErrorMessages(props.errors, props.errorsPgIds)
|
75
|
+
: undefined,
|
76
|
+
)
|
77
|
+
</script>
|
78
|
+
|
79
|
+
<i18n lang="yaml">
|
80
|
+
de:
|
81
|
+
submit: Absenden
|
82
|
+
en:
|
83
|
+
submit: Submit
|
84
|
+
</i18n>
|
@@ -0,0 +1,27 @@
|
|
1
|
+
<template>
|
2
|
+
<div>
|
3
|
+
<input
|
4
|
+
:id="`input-${formKey}`"
|
5
|
+
class="rounded"
|
6
|
+
type="checkbox"
|
7
|
+
:checked="value"
|
8
|
+
@change="emit('change', ($event.target as HTMLInputElement).checked)"
|
9
|
+
/>
|
10
|
+
<label class="pl-2" :for="`input-${formKey}`"><slot /></label>
|
11
|
+
</div>
|
12
|
+
</template>
|
13
|
+
|
14
|
+
<script setup lang="ts">
|
15
|
+
export interface Props {
|
16
|
+
formKey?: string
|
17
|
+
value?: boolean
|
18
|
+
}
|
19
|
+
withDefaults(defineProps<Props>(), {
|
20
|
+
formKey: undefined,
|
21
|
+
value: undefined,
|
22
|
+
})
|
23
|
+
|
24
|
+
const emit = defineEmits<{
|
25
|
+
change: [change: boolean]
|
26
|
+
}>()
|
27
|
+
</script>
|
@@ -0,0 +1,192 @@
|
|
1
|
+
<template>
|
2
|
+
<div>
|
3
|
+
<div
|
4
|
+
:class="{
|
5
|
+
'form-input-success': success,
|
6
|
+
'form-input-warning': warning,
|
7
|
+
'form-input-error': value?.$error,
|
8
|
+
}"
|
9
|
+
class="flex-wrap md:flex md:items-center"
|
10
|
+
>
|
11
|
+
<div class="mb-1 md:mb-0 md:w-1/3 md:pr-4 md:text-right">
|
12
|
+
<label
|
13
|
+
class="inline-flex items-baseline gap-2 font-semibold md:flex-col md:items-end md:gap-0"
|
14
|
+
:class="{
|
15
|
+
'form-input-success': success,
|
16
|
+
'form-input-warning': warning,
|
17
|
+
'form-input-error': value?.$error,
|
18
|
+
}"
|
19
|
+
:for="idLabel"
|
20
|
+
>
|
21
|
+
<span>{{ title }}</span>
|
22
|
+
<span
|
23
|
+
class="text-xs font-medium text-gray-500 dark:text-gray-400 md:text-right"
|
24
|
+
>
|
25
|
+
<span v-if="isRequired">
|
26
|
+
{{ t('required') }}
|
27
|
+
</span>
|
28
|
+
<span v-if="isOptional">
|
29
|
+
{{ t('optional') }}
|
30
|
+
</span>
|
31
|
+
</span>
|
32
|
+
</label>
|
33
|
+
</div>
|
34
|
+
<div class="flex md:mt-1 md:w-2/3">
|
35
|
+
<div class="relative min-w-0 grow">
|
36
|
+
<slot v-if="$slots.default" />
|
37
|
+
<input
|
38
|
+
v-else
|
39
|
+
:id="idLabel"
|
40
|
+
class="form-input"
|
41
|
+
:class="{
|
42
|
+
'rounded-r-none': $slots.icon,
|
43
|
+
}"
|
44
|
+
:disabled="isDisabled"
|
45
|
+
:placeholder="placeholder"
|
46
|
+
:readonly="isReadonly"
|
47
|
+
:type="type"
|
48
|
+
:value="valueFormatter(value?.$model as string)"
|
49
|
+
@input="emit('input', ($event.target as HTMLInputElement)?.value)"
|
50
|
+
@click="emit('click')"
|
51
|
+
/>
|
52
|
+
<div v-if="validationProperty && isValidatable">
|
53
|
+
<VioFormInputIconWrapper v-if="validationProperty.$pending">
|
54
|
+
<IconHourglass
|
55
|
+
class="text-blue-600"
|
56
|
+
:title="t('globalLoading')"
|
57
|
+
/>
|
58
|
+
</VioFormInputIconWrapper>
|
59
|
+
<VioFormInputIconWrapper
|
60
|
+
v-else-if="
|
61
|
+
validationProperty.$model && !validationProperty.$invalid
|
62
|
+
"
|
63
|
+
>
|
64
|
+
<IconCheckCircle class="text-green-600" :title="t('valid')" />
|
65
|
+
</VioFormInputIconWrapper>
|
66
|
+
<VioFormInputIconWrapper
|
67
|
+
v-else-if="
|
68
|
+
validationProperty.$model && validationProperty.$invalid
|
69
|
+
"
|
70
|
+
>
|
71
|
+
<IconExclamationCircle
|
72
|
+
class="text-red-600"
|
73
|
+
:title="t('validNot')"
|
74
|
+
/>
|
75
|
+
</VioFormInputIconWrapper>
|
76
|
+
</div>
|
77
|
+
</div>
|
78
|
+
<span
|
79
|
+
v-if="$slots.icon"
|
80
|
+
class="inline-flex cursor-pointer items-center rounded-r-md border border-l-0 border-gray-300 bg-gray-50 px-3 text-sm text-gray-600"
|
81
|
+
@click="emit('icon')"
|
82
|
+
>
|
83
|
+
<slot name="icon" />
|
84
|
+
</span>
|
85
|
+
</div>
|
86
|
+
<div class="md:w-1/3" />
|
87
|
+
<div class="md:w-2/3">
|
88
|
+
<slot name="stateSuccess" />
|
89
|
+
</div>
|
90
|
+
<div class="md:w-1/3" />
|
91
|
+
<div class="md:w-2/3">
|
92
|
+
<slot name="stateInfo" />
|
93
|
+
<VioFormInputStateInfo v-if="value?.$pending">
|
94
|
+
{{ t('globalLoading') }}
|
95
|
+
</VioFormInputStateInfo>
|
96
|
+
</div>
|
97
|
+
<div class="md:w-1/3" />
|
98
|
+
<div class="md:w-2/3">
|
99
|
+
<slot name="stateWarning" />
|
100
|
+
</div>
|
101
|
+
<div class="md:w-1/3" />
|
102
|
+
<div class="md:w-2/3">
|
103
|
+
<slot name="stateError" />
|
104
|
+
</div>
|
105
|
+
</div>
|
106
|
+
</div>
|
107
|
+
</template>
|
108
|
+
|
109
|
+
<script setup lang="ts">
|
110
|
+
import type { BaseValidation } from '@vuelidate/core'
|
111
|
+
import { consola } from 'consola'
|
112
|
+
|
113
|
+
export interface Props {
|
114
|
+
isDisabled?: boolean
|
115
|
+
isOptional?: boolean
|
116
|
+
isReadonly?: boolean
|
117
|
+
isRequired?: boolean
|
118
|
+
isValidatable?: boolean
|
119
|
+
idLabel?: string
|
120
|
+
placeholder?: string
|
121
|
+
success?: boolean
|
122
|
+
title?: string
|
123
|
+
type?: string
|
124
|
+
validationProperty?: BaseValidation
|
125
|
+
value?: BaseValidation
|
126
|
+
valueFormatter?: (x?: string) => typeof x | undefined
|
127
|
+
warning?: boolean
|
128
|
+
}
|
129
|
+
const props = withDefaults(defineProps<Props>(), {
|
130
|
+
isDisabled: false,
|
131
|
+
isOptional: false,
|
132
|
+
isReadonly: false,
|
133
|
+
isRequired: false,
|
134
|
+
isValidatable: false,
|
135
|
+
idLabel: undefined,
|
136
|
+
placeholder: undefined,
|
137
|
+
success: false,
|
138
|
+
title: undefined,
|
139
|
+
type: undefined,
|
140
|
+
validationProperty: undefined,
|
141
|
+
value: undefined,
|
142
|
+
valueFormatter: (x?: string) => x,
|
143
|
+
warning: false,
|
144
|
+
})
|
145
|
+
const typeProp = toRef(() => props.type)
|
146
|
+
|
147
|
+
const emit = defineEmits<{
|
148
|
+
icon: []
|
149
|
+
input: [input: string]
|
150
|
+
click: []
|
151
|
+
}>()
|
152
|
+
|
153
|
+
const { t } = useI18n()
|
154
|
+
|
155
|
+
// initialization
|
156
|
+
if (
|
157
|
+
!props.placeholder &&
|
158
|
+
typeProp.value &&
|
159
|
+
![
|
160
|
+
'checkbox',
|
161
|
+
'datetime-local',
|
162
|
+
'number',
|
163
|
+
'select',
|
164
|
+
'textarea',
|
165
|
+
'tiptap',
|
166
|
+
'radio',
|
167
|
+
].includes(typeProp.value)
|
168
|
+
) {
|
169
|
+
consola.warn(`placeholder is missing for ${props.idLabel}!`)
|
170
|
+
}
|
171
|
+
|
172
|
+
if (
|
173
|
+
!props.value &&
|
174
|
+
typeProp.value &&
|
175
|
+
!['checkbox', 'select'].includes(typeProp.value)
|
176
|
+
) {
|
177
|
+
consola.warn(`value is missing for ${props.idLabel}!`)
|
178
|
+
}
|
179
|
+
</script>
|
180
|
+
|
181
|
+
<i18n lang="yaml">
|
182
|
+
de:
|
183
|
+
optional: optional
|
184
|
+
required: Pflichtfeld
|
185
|
+
valid: Gültig
|
186
|
+
validNot: Ungültig
|
187
|
+
en:
|
188
|
+
optional: optional
|
189
|
+
required: required
|
190
|
+
valid: valid
|
191
|
+
validNot: invalid
|
192
|
+
</i18n>
|
@@ -0,0 +1,54 @@
|
|
1
|
+
<template>
|
2
|
+
<VioFormInput
|
3
|
+
v-if="formInput"
|
4
|
+
:is-optional="isOptional"
|
5
|
+
:id-label="`input-${id}`"
|
6
|
+
:placeholder="t('globalPlaceholderUrl')"
|
7
|
+
:title="t('url')"
|
8
|
+
type="url"
|
9
|
+
:value="formInput"
|
10
|
+
@input="emit('input', $event)"
|
11
|
+
>
|
12
|
+
<template #stateError>
|
13
|
+
<VioFormInputStateError
|
14
|
+
:form-input="formInput"
|
15
|
+
validation-property="maxLength"
|
16
|
+
>
|
17
|
+
{{ t('globalValidationLength') }}
|
18
|
+
</VioFormInputStateError>
|
19
|
+
<VioFormInputStateError
|
20
|
+
:form-input="formInput"
|
21
|
+
validation-property="formatUrlHttps"
|
22
|
+
>
|
23
|
+
{{ t('globalValidationFormatUrlHttps') }}
|
24
|
+
</VioFormInputStateError>
|
25
|
+
</template>
|
26
|
+
</VioFormInput>
|
27
|
+
</template>
|
28
|
+
|
29
|
+
<script setup lang="ts">
|
30
|
+
import type { BaseValidation } from '@vuelidate/core'
|
31
|
+
|
32
|
+
export interface Props {
|
33
|
+
formInput: BaseValidation
|
34
|
+
id?: string
|
35
|
+
isOptional?: boolean
|
36
|
+
}
|
37
|
+
withDefaults(defineProps<Props>(), {
|
38
|
+
id: 'phone-number',
|
39
|
+
isOptional: false,
|
40
|
+
})
|
41
|
+
|
42
|
+
const emit = defineEmits<{
|
43
|
+
input: [event: string]
|
44
|
+
}>()
|
45
|
+
|
46
|
+
const { t } = useI18n()
|
47
|
+
</script>
|
48
|
+
|
49
|
+
<i18n lang="yaml">
|
50
|
+
de:
|
51
|
+
url: Weblink
|
52
|
+
en:
|
53
|
+
url: Weblink
|
54
|
+
</i18n>
|