- TRA-72 - Translation of privacy statement and T&C - TRA-73 - Strange characters in Tenant Make Name - Addition of meta information in Specialist Form Fields
554 lines
18 KiB
Vue
554 lines
18 KiB
Vue
<template>
|
|
<div class="form-field" :class="{ 'radio-field': fieldType === 'radio' }" style="margin-bottom: 15px;">
|
|
<!-- Label voor alle velden behalve boolean/checkbox die een speciale behandeling krijgen -->
|
|
<label v-if="fieldType !== 'checkbox'" :for="fieldId" class="field-label" :class="{ 'tooltip-label': description }">
|
|
{{ field.name }}
|
|
<span v-if="field.required" class="required" style="color: #d93025; margin-left: 2px;">*</span>
|
|
<!-- Tooltip voor description -->
|
|
<span v-if="description" class="tooltip-content">{{ description }}</span>
|
|
</label>
|
|
|
|
<!-- Container voor input velden -->
|
|
<div style="width: 100%;">
|
|
<!-- Context informatie indien aanwezig -->
|
|
<div v-if="field.context" class="field-context">
|
|
{{ field.context }}
|
|
</div>
|
|
|
|
<!-- Tekstinvoer (string/str) -->
|
|
<input
|
|
v-if="fieldType === 'text'"
|
|
:id="fieldId"
|
|
type="text"
|
|
v-model="value"
|
|
:required="field.required"
|
|
:placeholder="field.placeholder || ''"
|
|
:title="description"
|
|
@keydown.enter="handleEnterKey"
|
|
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd; box-sizing: border-box;"
|
|
>
|
|
|
|
<!-- Numerieke invoer (int/float) -->
|
|
<input
|
|
v-if="fieldType === 'number'"
|
|
:id="fieldId"
|
|
type="number"
|
|
v-model.number="value"
|
|
:required="field.required"
|
|
:step="stepValue"
|
|
:placeholder="field.placeholder || ''"
|
|
:title="description"
|
|
@keydown.enter="handleEnterKey"
|
|
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd; box-sizing: border-box;"
|
|
>
|
|
|
|
<!-- Tekstvlak (text) -->
|
|
<textarea
|
|
v-if="fieldType === 'textarea'"
|
|
:id="fieldId"
|
|
v-model="value"
|
|
:required="field.required"
|
|
:rows="field.rows || 3"
|
|
:placeholder="field.placeholder || ''"
|
|
:title="description"
|
|
@keydown="handleTextareaKeydown"
|
|
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd; box-sizing: border-box; resize: vertical;"
|
|
></textarea>
|
|
|
|
<!-- Selectielijst (enum) -->
|
|
<select
|
|
v-if="fieldType === 'select'"
|
|
:id="fieldId"
|
|
v-model="value"
|
|
:required="field.required"
|
|
:title="description"
|
|
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd; box-sizing: border-box;"
|
|
>
|
|
<option value="">Selecteer een optie</option>
|
|
<option
|
|
v-for="option in (field.allowedValues || field.allowed_values || [])"
|
|
:key="option"
|
|
:value="option"
|
|
>
|
|
{{ option }}
|
|
</option>
|
|
</select>
|
|
|
|
<!-- Radio buttons (options) -->
|
|
<div v-if="fieldType === 'radio'" class="radio-group">
|
|
<label
|
|
v-for="option in (field.allowedValues || field.allowed_values || [])"
|
|
:key="option"
|
|
class="radio-label"
|
|
>
|
|
<input
|
|
type="radio"
|
|
:name="fieldId"
|
|
:value="option"
|
|
v-model="value"
|
|
:required="field.required"
|
|
>
|
|
<span>{{ option }}</span>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Checkbox (boolean) -->
|
|
<div v-if="fieldType === 'checkbox'" class="checkbox-container" style="display: flex; align-items: center;">
|
|
<label class="checkbox-label" style="display: flex; align-items: center; cursor: pointer;">
|
|
<input
|
|
type="checkbox"
|
|
:id="fieldId"
|
|
v-model="value"
|
|
:required="field.required"
|
|
style="margin-right: 8px;"
|
|
>
|
|
<!-- Regular checkbox label -->
|
|
<span v-if="!isConsentField" class="checkbox-text">{{ field.name }}</span>
|
|
<!-- Consent field with privacy and terms links (rich, multilingual) -->
|
|
<ConsentRichText
|
|
v-else
|
|
class="checkbox-text consent-text"
|
|
:template="texts.consentRich"
|
|
:aria-privacy="texts.ariaPrivacy || 'Open privacy statement in a dialog'"
|
|
:aria-terms="texts.ariaTerms || 'Open terms and conditions in a dialog'"
|
|
@open-privacy="openPrivacyModal"
|
|
@open-terms="openTermsModal"
|
|
/>
|
|
<span v-if="field.required" class="required" style="color: #d93025; margin-left: 2px;">*</span>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Bestandsupload -->
|
|
<input
|
|
v-if="fieldType === 'file'"
|
|
:id="fieldId"
|
|
type="file"
|
|
@change="handleFileUpload"
|
|
:required="field.required"
|
|
:accept="field.accept || '*'"
|
|
:title="description"
|
|
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd; box-sizing: border-box;"
|
|
>
|
|
|
|
<!-- Datum invoer -->
|
|
<input
|
|
v-if="fieldType === 'date'"
|
|
:id="fieldId"
|
|
type="date"
|
|
v-model="value"
|
|
:required="field.required"
|
|
:title="description"
|
|
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd; box-sizing: border-box;"
|
|
>
|
|
|
|
<!-- Tijd invoer -->
|
|
<input
|
|
v-if="fieldType === 'time'"
|
|
:id="fieldId"
|
|
type="time"
|
|
v-model="value"
|
|
:required="field.required"
|
|
:title="description"
|
|
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd; box-sizing: border-box;"
|
|
>
|
|
|
|
<!-- Email invoer -->
|
|
<input
|
|
v-if="fieldType === 'email'"
|
|
:id="fieldId"
|
|
type="email"
|
|
v-model="value"
|
|
:required="field.required"
|
|
:placeholder="field.placeholder || ''"
|
|
:title="description"
|
|
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd; box-sizing: border-box;"
|
|
>
|
|
|
|
<!-- URL invoer -->
|
|
<input
|
|
v-if="fieldType === 'url'"
|
|
:id="fieldId"
|
|
type="url"
|
|
v-model="value"
|
|
:required="field.required"
|
|
:placeholder="field.placeholder || ''"
|
|
:title="description"
|
|
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd; box-sizing: border-box;"
|
|
>
|
|
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { useComponentTranslations } from '../js/services/LanguageProvider.js';
|
|
import ConsentRichText from './ConsentRichText.vue';
|
|
|
|
export default {
|
|
name: 'FormField',
|
|
components: { ConsentRichText },
|
|
props: {
|
|
field: {
|
|
type: Object,
|
|
required: true,
|
|
validator: (field) => {
|
|
return field.name && field.type;
|
|
}
|
|
},
|
|
fieldId: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
modelValue: {
|
|
default: null
|
|
}
|
|
},
|
|
emits: ['update:modelValue', 'open-privacy-modal', 'open-terms-modal', 'keydown-enter'],
|
|
setup() {
|
|
// Consent text constants (English base) - rich template
|
|
const consentTexts = {
|
|
consentRich: "I agree with the <privacy>privacy statement</privacy> and the <terms>terms and conditions</terms>",
|
|
ariaPrivacy: 'Open privacy statement in a dialog',
|
|
ariaTerms: 'Open terms and conditions in a dialog'
|
|
};
|
|
|
|
try {
|
|
// Initialize translations for this component
|
|
const { texts } = useComponentTranslations('FormField', consentTexts);
|
|
|
|
return {
|
|
translatedTexts: texts,
|
|
fallbackTexts: consentTexts
|
|
};
|
|
} catch (error) {
|
|
console.error('FormField setup(): Error in useComponentTranslations:', error);
|
|
|
|
// Return fallback texts if LanguageProvider fails
|
|
return {
|
|
translatedTexts: null,
|
|
fallbackTexts: consentTexts
|
|
};
|
|
}
|
|
},
|
|
computed: {
|
|
texts() {
|
|
// Validate that consentRich exists and includes both required tags; otherwise fallback to English base
|
|
const hasValidRich = (t) => t && typeof t.consentRich === 'string'
|
|
&& /<privacy>[\s\S]*?<\/privacy>/.test(t.consentRich)
|
|
&& /<terms>[\s\S]*?<\/terms>/.test(t.consentRich);
|
|
|
|
// 1) Prefer backend-provided rich string on the field's meta (already localized)
|
|
const meta = this.field && this.field.meta ? this.field.meta : (this.field.i18n || null);
|
|
if (meta && typeof meta.consentRich === 'string' && hasValidRich(meta)) {
|
|
return {
|
|
consentRich: meta.consentRich,
|
|
ariaPrivacy: meta.ariaPrivacy || this.fallbackTexts.ariaPrivacy,
|
|
ariaTerms: meta.ariaTerms || this.fallbackTexts.ariaTerms
|
|
};
|
|
}
|
|
|
|
// 2) Otherwise, use client-side translated texts if available and valid
|
|
if (this.translatedTexts && typeof this.translatedTexts === 'object' && hasValidRich(this.translatedTexts)) {
|
|
return this.translatedTexts;
|
|
}
|
|
|
|
// 3) Fallback to English texts (rich template)
|
|
if (this.fallbackTexts && hasValidRich(this.fallbackTexts)) {
|
|
return this.fallbackTexts;
|
|
}
|
|
|
|
// 4) Ultimate fallback (should not happen): provide a safe default
|
|
return {
|
|
consentRich: "I agree with the <privacy>privacy statement</privacy> and the <terms>terms and conditions</terms>",
|
|
ariaPrivacy: 'Open privacy statement in a dialog',
|
|
ariaTerms: 'Open terms and conditions in a dialog'
|
|
};
|
|
},
|
|
value: {
|
|
get() {
|
|
// Gebruik default waarde als modelValue undefined is
|
|
if (this.modelValue === undefined || this.modelValue === null) {
|
|
if (this.field.type === 'boolean') {
|
|
return this.field.default === true;
|
|
}
|
|
return this.field.default !== undefined ? this.field.default : '';
|
|
}
|
|
return this.modelValue;
|
|
},
|
|
set(value) {
|
|
// Type conversie voor boolean velden
|
|
let processedValue = value;
|
|
if (this.field.type === 'boolean') {
|
|
// Converteer alle mogelijke waarden naar echte boolean
|
|
console.log('FormField Boolean Value: ', value);
|
|
processedValue = Boolean(value);
|
|
console.log('FormField Boolean Processed Value: ', processedValue);
|
|
}
|
|
|
|
// Voorkom emit als de waarde niet is veranderd
|
|
if (JSON.stringify(processedValue) !== JSON.stringify(this.modelValue)) {
|
|
this.$emit('update:modelValue', processedValue);
|
|
}
|
|
}
|
|
},
|
|
fieldType() {
|
|
// Map Python types naar HTML input types
|
|
const typeMap = {
|
|
'str': 'text',
|
|
'string': 'text',
|
|
'int': 'number',
|
|
'integer': 'number',
|
|
'float': 'number',
|
|
'text': 'textarea',
|
|
'enum': 'select',
|
|
'options': 'radio',
|
|
'boolean': 'checkbox',
|
|
'file': 'file',
|
|
'date': 'date',
|
|
'time': 'time',
|
|
'email': 'email',
|
|
'url': 'url'
|
|
};
|
|
return typeMap[this.field.type] || this.field.type;
|
|
},
|
|
stepValue() {
|
|
return this.field.type === 'float' ? 'any' : 1;
|
|
},
|
|
description() {
|
|
return this.field.description || '';
|
|
},
|
|
isConsentField() {
|
|
// Detect consent fields by fieldId (key in dictionary) only, not by name (translated label)
|
|
return this.field.type === 'boolean' &&
|
|
(this.fieldId === 'consent' ||
|
|
this.fieldId.toLowerCase().includes('consent'));
|
|
}
|
|
},
|
|
methods: {
|
|
handleFileUpload(event) {
|
|
const file = event.target.files[0];
|
|
if (file) {
|
|
this.value = file;
|
|
}
|
|
},
|
|
openPrivacyModal() {
|
|
this.$emit('open-privacy-modal');
|
|
},
|
|
openTermsModal() {
|
|
this.$emit('open-terms-modal');
|
|
},
|
|
|
|
// Handle Enter key press for text and number inputs
|
|
handleEnterKey(event) {
|
|
console.log('FormField: Enter pressed in field:', this.fieldId);
|
|
event.preventDefault();
|
|
this.$emit('keydown-enter');
|
|
},
|
|
|
|
// Handle keydown for textarea (Enter to submit, Shift+Enter for line breaks)
|
|
handleTextareaKeydown(event) {
|
|
console.log('FormField: Textarea keydown in field:', this.fieldId, 'Key:', event.key, 'Ctrl:', event.ctrlKey, 'Shift:', event.shiftKey);
|
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
// Plain Enter submits the form
|
|
console.log('FormField: Textarea Enter triggered for field:', this.fieldId);
|
|
event.preventDefault();
|
|
this.$emit('keydown-enter');
|
|
}
|
|
// Shift+Enter allows line breaks in textarea
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Form field layout */
|
|
.form-field {
|
|
margin-bottom: 15px;
|
|
display: grid;
|
|
grid-template-columns: 35% 65%;
|
|
align-items: start;
|
|
gap: 10px;
|
|
}
|
|
|
|
/* Radio field special layout - full width for better alignment */
|
|
.form-field.radio-field {
|
|
display: block;
|
|
}
|
|
|
|
.form-field.radio-field .field-label {
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.form-field.radio-field .field-context {
|
|
margin-left: 0;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
/* Field label styling */
|
|
.field-label {
|
|
margin-right: 10px;
|
|
font-weight: 500;
|
|
position: relative;
|
|
}
|
|
|
|
/* Tooltip functionality */
|
|
.tooltip-label {
|
|
cursor: help;
|
|
}
|
|
|
|
.tooltip-content {
|
|
visibility: hidden;
|
|
opacity: 0;
|
|
position: absolute;
|
|
z-index: 1000;
|
|
bottom: 125%;
|
|
left: 0;
|
|
background-color: #333;
|
|
color: white;
|
|
text-align: left;
|
|
padding: 8px 12px;
|
|
border-radius: 6px;
|
|
font-size: 0.8rem;
|
|
font-weight: normal;
|
|
line-height: 1.4;
|
|
white-space: normal;
|
|
max-width: 300px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
|
transition: opacity 0.3s, visibility 0.3s;
|
|
}
|
|
|
|
.tooltip-content::after {
|
|
content: "";
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 20px;
|
|
margin-left: -5px;
|
|
border-width: 5px;
|
|
border-style: solid;
|
|
border-color: #333 transparent transparent transparent;
|
|
}
|
|
|
|
.tooltip-label:hover .tooltip-content {
|
|
visibility: visible;
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Input field focus styling */
|
|
.form-field input:focus,
|
|
.form-field select:focus,
|
|
.form-field textarea:focus {
|
|
outline: none;
|
|
border-color: #4a90e2 !important;
|
|
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
|
|
}
|
|
|
|
/* Radio button styling */
|
|
.radio-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.radio-label[data-v-8b0edd] {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-start;
|
|
gap: 12px;
|
|
width: 100%;
|
|
}
|
|
|
|
.radio-label input[type="radio"] {
|
|
flex: 0 0 20px; /* fixed width of 20px, no growing/shrinking */
|
|
height: 13px;
|
|
width: 13px;
|
|
margin: 0;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.radio-label span {
|
|
flex: 1; /* take up all remaining space */
|
|
word-break: break-word; /* break long words if needed */
|
|
margin-left: 12px;
|
|
}
|
|
|
|
/* Checkbox styling */
|
|
.checkbox-container {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-start;
|
|
}
|
|
|
|
.checkbox-label {
|
|
display: flex;
|
|
align-items: center;
|
|
cursor: pointer;
|
|
text-align: left;
|
|
}
|
|
|
|
.checkbox-text {
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
/* Context field styling */
|
|
.field-context {
|
|
margin-bottom: 8px;
|
|
font-size: 0.9rem;
|
|
padding: 8px;
|
|
border-radius: 4px;
|
|
text-align: left;
|
|
}
|
|
|
|
.required {
|
|
color: #d93025;
|
|
margin-left: 2px;
|
|
}
|
|
|
|
/* Consent field styling */
|
|
.consent-text {
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.consent-link {
|
|
color: #007bff;
|
|
text-decoration: underline;
|
|
cursor: pointer;
|
|
transition: color 0.2s ease;
|
|
}
|
|
|
|
.consent-link:hover {
|
|
color: #0056b3;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.consent-link:focus {
|
|
outline: 2px solid #007bff;
|
|
outline-offset: 2px;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
/* Responsive adjustments */
|
|
@media (max-width: 768px) {
|
|
.form-field {
|
|
display: block;
|
|
grid-template-columns: none;
|
|
}
|
|
|
|
.form-field .field-label {
|
|
margin-bottom: 8px;
|
|
display: block;
|
|
}
|
|
|
|
.tooltip-content {
|
|
position: fixed;
|
|
left: 10px;
|
|
right: 10px;
|
|
max-width: none;
|
|
bottom: auto;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
}
|
|
}
|
|
</style> |