@dargmuesli/nuxt-vio 2.0.1 → 3.0.0-beta.10
Sign up to get free protection for your applications and to get access to all the features.
- package/app.config.ts +83 -40
- package/components/vio/_/VioApp.vue +92 -0
- package/components/{VioError.vue → vio/_/VioError.vue} +1 -1
- package/components/{VioLink.vue → vio/_/VioLink.vue} +2 -2
- package/components/vio/button/VioButtonColored.vue +52 -0
- package/components/vio/card/VioCard.vue +19 -0
- package/components/vio/card/state/VioCardState.vue +20 -0
- package/components/vio/card/state/VioCardStateAlert.vue +14 -0
- package/components/vio/form/VioForm.vue +84 -0
- package/components/vio/form/VioFormCheckbox.vue +27 -0
- package/components/vio/form/input/VioFormInput.vue +192 -0
- package/components/vio/form/input/VioFormInputIconWrapper.vue +7 -0
- package/components/vio/form/input/VioFormInputUrl.vue +54 -0
- package/components/vio/form/input/state/VioFormInputState.vue +5 -0
- package/components/vio/form/input/state/VioFormInputStateError.vue +32 -0
- package/components/vio/form/input/state/VioFormInputStateInfo.vue +32 -0
- package/components/vio/icon/IconArrowRight.vue +31 -0
- package/components/vio/icon/IconCalendar.vue +31 -0
- package/components/vio/icon/IconChatOutline.vue +27 -0
- package/components/vio/icon/IconChatSolid.vue +26 -0
- package/components/vio/icon/IconCheckCircle.vue +29 -0
- package/components/vio/icon/IconContainer.vue +15 -0
- package/components/vio/icon/IconDownload.vue +31 -0
- package/components/vio/icon/IconExclamationCircle.vue +29 -0
- package/components/vio/icon/IconHome.vue +31 -0
- package/components/vio/icon/IconHourglass.vue +32 -0
- package/components/vio/icon/IconLightbulb.vue +27 -0
- package/components/vio/icon/IconLogo.vue +17 -0
- package/components/vio/icon/IconMixcloud.vue +23 -0
- package/components/vio/icon/IconMusic.vue +27 -0
- package/components/vio/icon/IconPlay.vue +25 -0
- package/components/vio/icon/IconShare.vue +27 -0
- package/components/{VioLayout.vue → vio/layout/VioLayout.vue} +1 -1
- package/components/vio/layout/VioLayoutBreadcrumbs.vue +83 -0
- package/components/vio/layout/VioLayoutFooter.vue +40 -0
- package/components/vio/layout/VioLayoutFooterCategory.vue +17 -0
- package/components/vio/layout/VioLayoutHeader.vue +98 -0
- package/components/vio/layout/VioLayoutSpanList.vue +20 -0
- package/components/vio/loader/indicator/VioLoaderIndicator.vue +14 -0
- package/components/vio/loader/indicator/VioLoaderIndicatorPing.vue +12 -0
- package/components/vio/loader/indicator/VioLoaderIndicatorSpinner.vue +24 -0
- package/components/{VioLegalNotice.vue → vio/page/VioPageLegalNotice.vue} +10 -8
- package/components/{VioPrivacyPolicy.vue → vio/page/VioPagePrivacyPolicy.vue} +19 -12
- package/composables/useAppLayout.ts +12 -18
- package/composables/useDateTime.ts +17 -0
- package/composables/useFavicons.ts +5 -33
- package/composables/useFireError.ts +17 -0
- package/composables/useGetServiceHref.ts +21 -0
- package/composables/useHeadDefault.ts +21 -0
- package/composables/usePolyfills.ts +23 -0
- package/composables/useStrapiFetch.ts +10 -0
- package/error.vue +5 -2
- package/locales/de.json +7 -1
- package/locales/en.json +7 -1
- package/nuxt.config.ts +56 -10
- package/package.json +37 -25
- package/pages/legal-notice.vue +1 -1
- package/pages/privacy-policy.vue +1 -1
- package/plugins/dayjs.ts +34 -0
- package/plugins/gtag.client.ts +3 -0
- package/plugins/i18n.ts +5 -0
- package/plugins/marked.ts +9 -0
- package/server/middleware/headers.ts +41 -10
- package/server/tsconfig.json +1 -1
- package/server/utils/util.ts +2 -0
- package/store/auth.ts +32 -0
- package/tailwind.config.ts +131 -10
- package/types/api.d.ts +9 -0
- package/types/fetch.d.ts +8 -0
- package/types/modules/gql.d.ts +6 -0
- package/types/modules/graphql.d.ts +6 -0
- package/utils/constants.ts +10 -1
- package/utils/form.ts +19 -0
- package/utils/networking.ts +117 -0
- package/utils/text.ts +20 -0
- package/LICENSE +0 -674
- package/components/VioApp.vue +0 -59
- /package/components/{VioButton.vue → vio/button/VioButton.vue} +0 -0
- /package/components/{VioHr.vue → vio/layout/VioLayoutHr.vue} +0 -0
package/app.config.ts
CHANGED
@@ -1,57 +1,100 @@
|
|
1
|
-
import {
|
1
|
+
import { useServerSeoMeta } from '@unhead/vue'
|
2
2
|
|
3
3
|
export default defineAppConfig({
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
vio: {
|
5
|
+
pages: undefined,
|
6
|
+
seoMeta: undefined,
|
7
|
+
server: {
|
8
|
+
middleware: {
|
9
|
+
headers: {
|
10
|
+
csp: {
|
11
|
+
default: {
|
12
|
+
'Cross-Origin-Opener-Policy': 'same-origin',
|
13
|
+
// 'Cross-Origin-Embedder-Policy', 'require-corp') // https://stackoverflow.com/questions/71904052/getting-notsameoriginafterdefaultedtosameoriginbycoep-error-with-helmet
|
14
|
+
'Cross-Origin-Resource-Policy': 'same-origin',
|
15
|
+
// 'Expect-CT', 'max-age=0') // deprecated (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect-CT)
|
16
|
+
NEL: '\'{"report_to":"default","max_age":31536000,"include_subdomains":true}\'',
|
17
|
+
'Origin-Agent-Cluster': '?1',
|
18
|
+
'Permissions-Policy': '',
|
19
|
+
'Referrer-Policy': 'no-referrer',
|
20
|
+
'Report-To':
|
21
|
+
'\'{"group":"default":"max_age":31536000:"endpoints":[{"url":"https://dargmuesli.report-uri.com/a/d/g"}]:"include_subdomains":true}\'',
|
22
|
+
'X-Content-Type-Options': 'nosniff',
|
23
|
+
'X-DNS-Prefetch-Control': 'off',
|
24
|
+
'X-Download-Options': 'noopen',
|
25
|
+
'X-Frame-Options': 'SAMEORIGIN',
|
26
|
+
'X-Permitted-Cross-Domain-Policies': 'none',
|
27
|
+
'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)
|
28
|
+
},
|
29
|
+
production: {
|
30
|
+
'Strict-Transport-Security':
|
31
|
+
'max-age=31536000; includeSubDomains; preload',
|
32
|
+
},
|
33
|
+
},
|
34
|
+
},
|
35
|
+
},
|
36
|
+
},
|
37
|
+
themeColor: undefined,
|
8
38
|
},
|
9
|
-
themeColor: '#202020',
|
10
39
|
})
|
11
40
|
|
12
41
|
declare module 'nuxt/schema' {
|
13
42
|
interface AppConfigInput {
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
43
|
+
vio: {
|
44
|
+
pages?: {
|
45
|
+
legalNotice?: {
|
46
|
+
contact: {
|
47
|
+
email: string
|
48
|
+
}
|
49
|
+
responsibility: {
|
50
|
+
address: {
|
51
|
+
city: string
|
52
|
+
name: string
|
53
|
+
street: string
|
54
|
+
}
|
55
|
+
}
|
56
|
+
tmg: {
|
57
|
+
address: {
|
58
|
+
city: string
|
59
|
+
name: string
|
60
|
+
street: string
|
61
|
+
}
|
62
|
+
}
|
30
63
|
}
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
64
|
+
privacyPolicy?: {
|
65
|
+
hostingCdn?: {
|
66
|
+
external: {
|
67
|
+
address: {
|
68
|
+
city: string
|
69
|
+
name: string
|
70
|
+
street: string
|
71
|
+
}
|
72
|
+
}
|
73
|
+
}
|
74
|
+
mandatoryInfo?: {
|
75
|
+
responsible: {
|
76
|
+
address: {
|
77
|
+
city: string
|
78
|
+
email: string
|
79
|
+
name: string
|
80
|
+
street: string
|
81
|
+
}
|
82
|
+
}
|
40
83
|
}
|
41
84
|
}
|
42
85
|
}
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
86
|
+
seoMeta?: Parameters<typeof useServerSeoMeta>[0]
|
87
|
+
server?: {
|
88
|
+
middleware: {
|
89
|
+
headers: {
|
90
|
+
csp: {
|
91
|
+
default: Record<string, string>
|
92
|
+
production: Record<string, string>
|
93
|
+
}
|
50
94
|
}
|
51
95
|
}
|
52
96
|
}
|
97
|
+
themeColor?: string
|
53
98
|
}
|
54
|
-
seoMeta?: Parameters<typeof useSeoMeta>[0]
|
55
|
-
themeColor?: string
|
56
99
|
}
|
57
100
|
}
|
@@ -0,0 +1,92 @@
|
|
1
|
+
<template>
|
2
|
+
<div :data-is-loading="isLoading" data-testid="is-loading">
|
3
|
+
<NuxtLayout>
|
4
|
+
<!-- `NuxtLayout` can't have mulitple child nodes (https://github.com/nuxt/nuxt/issues/21759) -->
|
5
|
+
<div>
|
6
|
+
<NuxtPage :site-description="siteDescriptionProp" />
|
7
|
+
<CookieControl :locale="locale" />
|
8
|
+
</div>
|
9
|
+
</NuxtLayout>
|
10
|
+
</div>
|
11
|
+
</template>
|
12
|
+
|
13
|
+
<script setup lang="ts">
|
14
|
+
import type { Locale } from '@dargmuesli/nuxt-cookie-control/dist/runtime/types'
|
15
|
+
import type { WritableComputedRef } from 'vue'
|
16
|
+
|
17
|
+
export interface Props {
|
18
|
+
ogImageAlt: string
|
19
|
+
ogImageComponent?: string
|
20
|
+
siteDescription: string
|
21
|
+
}
|
22
|
+
const props = withDefaults(defineProps<Props>(), {
|
23
|
+
ogImageComponent: undefined,
|
24
|
+
})
|
25
|
+
const ogImageAltProp = toRef(() => props.ogImageAlt)
|
26
|
+
const ogImageComponentProp = toRef(() => props.ogImageComponent)
|
27
|
+
const siteDescriptionProp = toRef(() => props.siteDescription)
|
28
|
+
|
29
|
+
const { $dayjs } = useNuxtApp()
|
30
|
+
const i18n = useI18n()
|
31
|
+
const cookieControl = useCookieControl()
|
32
|
+
const siteConfig = useSiteConfig()
|
33
|
+
|
34
|
+
const { loadingIds, indicateLoadingDone } = useLoadingDoneIndicator('app')
|
35
|
+
|
36
|
+
// data
|
37
|
+
const locale = i18n.locale as WritableComputedRef<Locale>
|
38
|
+
|
39
|
+
// methods
|
40
|
+
const init = () => {
|
41
|
+
if (process.client) {
|
42
|
+
const cookieTimezone = useCookie(TIMEZONE_COOKIE_NAME, {
|
43
|
+
// default: () => undefined, // setting `default` on the client side only does not write the cookie
|
44
|
+
httpOnly: false,
|
45
|
+
sameSite: 'strict',
|
46
|
+
secure: true,
|
47
|
+
})
|
48
|
+
// @ts-ignore `tz` should be part of `$dayjs` (https://github.com/iamkun/dayjs/issues/2106)
|
49
|
+
cookieTimezone.value = $dayjs.tz.guess()
|
50
|
+
}
|
51
|
+
}
|
52
|
+
|
53
|
+
// computations
|
54
|
+
const isLoading = computed(() => !!loadingIds.value.length)
|
55
|
+
|
56
|
+
// lifecycle
|
57
|
+
onMounted(() => indicateLoadingDone())
|
58
|
+
watch(
|
59
|
+
() => cookieControl.cookiesEnabledIds.value,
|
60
|
+
(current, previous) => {
|
61
|
+
if (
|
62
|
+
(!previous?.includes('ga') && current?.includes('ga')) ||
|
63
|
+
(previous?.includes('ga') && !current?.includes('ga'))
|
64
|
+
) {
|
65
|
+
window.location.reload()
|
66
|
+
}
|
67
|
+
},
|
68
|
+
{ deep: true },
|
69
|
+
)
|
70
|
+
|
71
|
+
// initialization
|
72
|
+
updateSiteConfig({
|
73
|
+
description: siteDescriptionProp.value,
|
74
|
+
})
|
75
|
+
defineOgImage({
|
76
|
+
alt: ogImageAltProp.value,
|
77
|
+
component: ogImageComponentProp.value,
|
78
|
+
description: siteDescriptionProp.value,
|
79
|
+
})
|
80
|
+
useAppLayout()
|
81
|
+
useFavicons()
|
82
|
+
usePolyfills()
|
83
|
+
useSchemaOrg([
|
84
|
+
defineWebSite({
|
85
|
+
description: siteDescriptionProp,
|
86
|
+
inLanguage: locale,
|
87
|
+
name: siteConfig.name,
|
88
|
+
}),
|
89
|
+
defineWebPage(),
|
90
|
+
])
|
91
|
+
init()
|
92
|
+
</script>
|
@@ -12,7 +12,7 @@
|
|
12
12
|
>
|
13
13
|
<slot />
|
14
14
|
</a>
|
15
|
-
<
|
15
|
+
<NuxtLinkLocale
|
16
16
|
v-else
|
17
17
|
:aria-label="ariaLabel"
|
18
18
|
:class="classes"
|
@@ -20,7 +20,7 @@
|
|
20
20
|
@click="emit('click')"
|
21
21
|
>
|
22
22
|
<slot />
|
23
|
-
</
|
23
|
+
</NuxtLinkLocale>
|
24
24
|
</template>
|
25
25
|
|
26
26
|
<script setup lang="ts">
|
@@ -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('globalStatusLoading')"
|
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('globalStatusLoading') }}
|
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>
|