File: D:/web/matomo.vdk/plugins/CorePluginsAdmin/vue/src/FormField/FormField.vue
<!--
Matomo - free/libre analytics platform
@link https://matomo.org
@license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
-->
<template>
<div
class="form-group row matomo-form-field"
>
<h3
v-if="formField.introduction"
class="col s12"
>
{{ formField.introduction }}
</h3>
<div
class="col s12"
:class="{
'input-field': formField.uiControl !== 'checkbox' && formField.uiControl !== 'radio',
'file-field': formField.uiControl === 'file',
'm6': !formField.fullWidth,
}"
>
<component
:is="childComponent"
v-bind="{
formField,
...formField,
id: fieldId,
modelValue: processedModelValue,
modelModifiers,
availableOptions,
...extraChildComponentParams,
}"
@update:modelValue="onChange($event)"
@check:isValid="onCheckIsValid($event)"
>
</component>
</div>
<div
class="col s12"
:class="{ 'm6': !formField.fullWidth }"
>
<div
v-if="showFormHelp"
class="form-help"
>
<div
v-show="formField.description"
class="form-description"
>
{{ formField.description }}
</div>
<span
class="inline-help"
ref="inlineHelp"
v-if="formField.inlineHelp || hasInlineHelpSlot"
>
<component
v-if="inlineHelpComponent"
:is="inlineHelpComponent"
v-bind="inlineHelpBind"
/>
<slot name="inline-help"></slot>
</span>
<span v-show="showDefaultValue">
<br />
{{ translate('General_Default') }}:
<span>{{ defaultValuePrettyTruncated }}</span>
</span>
</div>
<Notification
v-if="isPrivacyPolicyControlled"
:noclear="true"
context="info"
>
{{ translate('PrivacyManager_PolicyControlledSetting') }} <a :href="privacyPolicyLink">
{{ translate('PrivacyManager_ViewPrivacyComplianceOverview') }}
</a>
</Notification>
</div>
</div>
</template>
<script lang="ts">
import {
defineComponent,
onMounted,
ref,
watch,
Component,
markRaw,
} from 'vue';
import { useExternalPluginComponent, MatomoUrl, Notification } from 'CoreHome';
import FieldCheckbox from './FieldCheckbox.vue';
import FieldCheckboxArray from './FieldCheckboxArray.vue';
import FieldExpandableSelect, {
getAvailableOptions as getExpandableSelectAvailableOptions,
} from './FieldExpandableSelect.vue';
import FieldFieldArray from './FieldFieldArray.vue';
import FieldFile from './FieldFile.vue';
import FieldHidden from './FieldHidden.vue';
import FieldMultituple from './FieldMultituple.vue';
import FieldNumber from './FieldNumber.vue';
import FieldRadio from './FieldRadio.vue';
import FieldSelect, {
getAvailableOptions as getSelectAvailableOptions,
} from './FieldSelect.vue';
import FieldSite from './FieldSite.vue';
import FieldText from './FieldText.vue';
import FieldTextArray from './FieldTextArray.vue';
import FieldTextarea from './FieldTextarea.vue';
import FieldTextareaArray from './FieldTextareaArray.vue';
import { processCheckboxAndRadioAvailableValues } from './utilities';
import FieldPassword from './FieldPassword.vue';
const TEXT_CONTROLS = ['url', 'search', 'email'];
const CONTROLS_SUPPORTING_ARRAY = ['textarea', 'checkbox', 'text'];
const CONTROL_TO_COMPONENT_MAP: Record<string, string> = {
checkbox: 'FieldCheckbox',
'expandable-select': 'FieldExpandableSelect',
'field-array': 'FieldFieldArray',
file: 'FieldFile',
hidden: 'FieldHidden',
multiselect: 'FieldSelect',
multituple: 'FieldMultituple',
number: 'FieldNumber',
password: 'FieldPassword',
radio: 'FieldRadio',
select: 'FieldSelect',
site: 'FieldSite',
text: 'FieldText',
textarea: 'FieldTextarea',
};
type ProcessAvailableOptionsFn = (
availableValues: Record<string, unknown>|null,
type: string,
uiControlAttributes?: Record<string, unknown>,
) => unknown[];
const CONTROL_TO_AVAILABLE_OPTION_PROCESSOR: Record<string, ProcessAvailableOptionsFn> = {
FieldSelect: getSelectAvailableOptions,
FieldCheckboxArray: processCheckboxAndRadioAvailableValues,
FieldRadio: processCheckboxAndRadioAvailableValues,
FieldExpandableSelect: getExpandableSelectAvailableOptions,
};
interface ComponentReference {
plugin: string;
name: string;
}
interface FormField {
availableValues: Record<string, unknown>;
type: string;
uiControlAttributes?: Record<string, unknown>;
defaultValue: unknown;
uiControl: string;
component: Component | ComponentReference;
inlineHelp?: string;
inlineHelpBind?: unknown;
}
interface OptionLike {
key?: string|number;
value?: unknown;
}
export default defineComponent({
props: {
modelValue: null,
modelModifiers: Object,
formField: {
type: Object,
required: true,
},
},
emits: ['update:modelValue', 'check:isValid'],
components: {
Notification,
FieldCheckbox,
FieldCheckboxArray,
FieldExpandableSelect,
FieldFieldArray,
FieldFile,
FieldHidden,
FieldMultituple,
FieldNumber,
FieldRadio,
FieldSelect,
FieldSite,
FieldText,
FieldTextArray,
FieldTextarea,
FieldTextareaArray,
FieldPassword,
},
setup(props) {
const inlineHelpNode = ref<HTMLElement|null>(null);
const setInlineHelp = (newVal?: string|HTMLElement|JQuery) => {
let toAppend: HTMLElement|JQuery|string;
if (!newVal
|| !inlineHelpNode.value
|| typeof (newVal as unknown as Record<string, unknown>).render === 'function'
) {
return;
}
if (typeof newVal === 'string') {
if (newVal.indexOf('#') === 0) {
toAppend = window.$(newVal);
} else {
toAppend = window.vueSanitize(newVal);
}
} else {
toAppend = newVal;
}
window.$(inlineHelpNode.value).html('').append(toAppend);
};
watch(() => (props.formField as FormField).inlineHelp, setInlineHelp);
onMounted(() => {
setInlineHelp((props.formField as FormField).inlineHelp);
});
return {
inlineHelp: inlineHelpNode,
};
},
computed: {
inlineHelpComponent() {
const formField = this.formField as FormField;
const inlineHelpRecord = formField.inlineHelp as unknown as Record<string, unknown>;
if (inlineHelpRecord && typeof inlineHelpRecord.render === 'function') {
return formField.inlineHelp as Component;
}
return undefined;
},
inlineHelpBind() {
return this.inlineHelpComponent ? this.formField.inlineHelpBind : undefined;
},
childComponent(): string|Component {
const formField = this.formField as FormField;
if (formField.component) {
let component = formField.component as Component;
if ((formField.component as ComponentReference).plugin) {
const { plugin, name } = formField.component as ComponentReference;
if (!plugin || !name) {
throw new Error('Invalid component property given to FormField directive, must be '
+ '{plugin: \'...\',name: \'...\'}');
}
component = useExternalPluginComponent(plugin, name);
}
return markRaw(component);
}
const { uiControl } = formField;
let control = CONTROL_TO_COMPONENT_MAP[uiControl];
if (TEXT_CONTROLS.indexOf(uiControl) !== -1) {
control = 'FieldText'; // we use same field for url, email etc.
}
if (this.formField.type === 'array' && CONTROLS_SUPPORTING_ARRAY.indexOf(uiControl) !== -1) {
control = `${control}Array`;
}
return control;
},
extraChildComponentParams() {
if (this.formField.uiControl === 'multiselect') {
return { multiple: true };
}
return {};
},
showFormHelp() {
return this.formField.description
|| this.formField.inlineHelp
|| this.showDefaultValue
|| this.hasInlineHelpSlot;
},
showDefaultValue() {
return this.defaultValuePretty
&& this.formField.uiControl !== 'checkbox'
&& this.formField.uiControl !== 'radio';
},
processedModelValue() {
const field = this.formField as FormField;
// handle boolean field types
if (field.type === 'boolean') {
const valueIsTruthy = this.modelValue && this.modelValue > 0 && this.modelValue !== '0';
// for checkboxes, the value MUST be either true or false
if (field.uiControl === 'checkbox') {
return valueIsTruthy;
}
if (field.uiControl === 'radio') {
return valueIsTruthy ? '1' : '0';
}
}
return this.modelValue;
},
defaultValue(): string {
const { defaultValue } = this.formField as FormField;
if (Array.isArray(defaultValue)) {
return (defaultValue as unknown[]).join(',');
}
return defaultValue as string;
},
availableOptions() {
const { childComponent } = this;
if (typeof childComponent !== 'string') {
return null;
}
const formField = this.formField as FormField;
if (!formField.availableValues
|| !CONTROL_TO_AVAILABLE_OPTION_PROCESSOR[childComponent]
) {
return null;
}
return CONTROL_TO_AVAILABLE_OPTION_PROCESSOR[childComponent](
formField.availableValues,
formField.type,
formField.uiControlAttributes,
);
},
defaultValuePretty() {
const formField = this.formField as FormField;
let { defaultValue } = formField;
const { availableOptions } = this;
if (typeof defaultValue === 'string' && defaultValue) {
// eg default value for multi tuple
let defaultParsed = null;
try {
defaultParsed = JSON.parse(defaultValue);
} catch (e) {
// invalid JSON
}
if (defaultParsed !== null && typeof defaultParsed === 'object') {
return '';
}
}
if (!Array.isArray(availableOptions)) {
if (Array.isArray(defaultValue)) {
return '';
}
return defaultValue ? `${defaultValue}` : '';
}
const prettyValues: unknown[] = [];
if (!Array.isArray(defaultValue)) {
defaultValue = [defaultValue];
}
(availableOptions || []).forEach((value) => {
if (typeof (value as OptionLike).value !== 'undefined'
&& (defaultValue as unknown[]).indexOf((value as OptionLike).key) !== -1
) {
prettyValues.push((value as OptionLike).value);
}
});
return prettyValues.join(', ');
},
defaultValuePrettyTruncated() {
return this.defaultValuePretty.substring(0, 50);
},
hasInlineHelpSlot() {
if (!this.$slots['inline-help']) {
return false;
}
const inlineHelpSlot = this.$slots['inline-help']();
return !!inlineHelpSlot?.[0]?.children?.length;
},
fieldId() {
return this.formField.id ? this.formField.id : this.formField.name;
},
getExtraMetadataIdSite() {
return this.formField.extraMetadata?.idSite;
},
isPrivacyPolicyControlled() {
return this.formField.extraMetadata?.compliancePolicyControlled !== undefined;
},
privacyPolicyLink() {
return `?${MatomoUrl.stringify({
...MatomoUrl.urlParsed.value,
module: 'PrivacyManager',
action: 'compliance',
idSite: this.getExtraMetadataIdSite ?? 'all',
})}`;
},
},
methods: {
onChange(newValue: unknown) {
this.$emit('update:modelValue', newValue);
},
onCheckIsValid(isValid: boolean) {
this.$emit('check:isValid', isValid);
},
},
});
</script>