Merge branch 'master' into feature-28575/Softwarebereitstellung_GUI_zur_Verwaltung_von_Software

# Conflicts:
#	public/js/components/filter/Filter.js
#	public/js/plugin/FhcAlert.js
#	system/filtersupdate.php
This commit is contained in:
Cris
2024-03-06 14:07:19 +01:00
42 changed files with 8361 additions and 137 deletions
+123
View File
@@ -0,0 +1,123 @@
import FhcFragment from "../Fragment.js";
export default {
components: {
FhcFragment
},
provide() {
return {
$registerToForm: component => {
if (this.inputs.indexOf(component) < 0)
this.inputs.push(component);
},
$clearValidationForName: this.clearValidationForName
};
},
props: {
tag: {
type: String,
default: 'form'
}
},
data() {
return {
inputs: []
}
},
computed: {
sortedInputs() {
return this.inputs.reduce((a,c) => {
let name = c.name || '_default';
if (!a[name])
a[name] = [];
a[name].push(c);
if (c.lcType == 'checkbox' && name.substr(-1) == ']' && name.indexOf('[')) {
name = name.substr(0, name.lastIndexOf('['));
if (!a[name])
a[name] = [];
a[name].push(c);
}
return a;
}, {});
},
factory() {
const factory = Object.create(Object.getPrototypeOf(this.$fhcApi.factory), Object.getOwnPropertyDescriptors(this.$fhcApi.factory));
factory.$fhcApi = {
get: this.get,
post: this.post
};
return factory;
}
},
methods: {
get(...args) {
if (typeof args[0] == 'object' && args[0].clearValidation && args[0].setFeedback)
args[0] = this;
else
args.unshift(this);
return this.$fhcApi.get(...args);
},
post(...args) {
if (typeof args[0] == 'object' && args[0].clearValidation && args[0].setFeedback)
args[0] = this;
else
args.unshift(this);
return this.$fhcApi.post(...args);
},
_sendFeedbackToInput(inputs, feedback, valid) {
if (inputs.length) {
inputs.forEach(input => input.setFeedback(valid, feedback));
return false;
}
if (this.$fhcAlert) {
this.$fhcAlert[valid ? 'alertSuccess' : 'alertError'](feedback);
return false;
}
return true;
},
setFeedback(valid, feedback) {
if (Array.isArray(feedback)) {
let remaining = feedback.filter(fb =>
this._sendFeedbackToInput(
this.sortedInputs['_default'] || [],
fb,
valid
)
);
return remaining.length ? remaining : null;
}
if (typeof feedback === 'object') {
let remaining = Object.entries(feedback).filter(([name, fb]) =>
this._sendFeedbackToInput(
this.sortedInputs[name.split('.')[0] + name.split('.').slice(1).map(p => `[${p}]`).join("")] || this.sortedInputs['_default'] || [],
fb,
valid
)
);
return remaining.length ? Object.fromEntries(remaining) : null;
}
let remaining = this._sendFeedbackToInput(
this.sortedInputs['_default'] || [],
feedback,
valid
);
return remaining ? feedback : null;
},
clearValidation() {
this.inputs.forEach(input => input.clearValidation());
},
clearValidationForName(name) {
(this.sortedInputs[name.split('.')[0] + name.split('.').slice(1).map(p => `[${p}]`).join("")] || this.sortedInputs['_default'] || [])
.forEach(input => input.clearValidation());
}
},
template: `
<component :is="tag || 'FhcFragment'" v-bind="$attrs">
<slot></slot>
</component>`
}
+315
View File
@@ -0,0 +1,315 @@
import FhcFragment from "../Fragment.js";
let _uuid = {};
export default {
inheritAttrs: false,
components: {
FhcFragment
},
inject: {
registerToForm: {
from: '$registerToForm',
default: null
},
clearValidationForName: {
from: '$clearValidationForName',
default: null
}
},
props: {
bsFeedback: Boolean,
noAutoClass: Boolean,
noFeedback: Boolean,
inputGroup: Boolean,
type: String,
name: String,
containerClass: [String, Array, Object]
},
data() {
return {
valid: undefined,
feedback: [],
modelValueDummy: undefined
}
},
computed: {
hasContainer() {
if (!this.bsFeedback)
return true;
if (this.containerClass)
return true;
for (const prop in this.autoContainerClass)
if (Object.hasOwn(this.autoContainerClass, prop))
return true;
return false;
},
acc() {
if (!this.containerClass)
return {};
if (typeof this.containerClass === 'string' || this.containerClass instanceof String)
return this.containerClass.split(' ').reduce((a,c) => {a[c] = true; return a}, {});
if (Array.isArray(this.containerClass))
return this.containerClass.reduce((a,c) => {a[c] = true; return a}, {});
return this.containerClass;
},
autoContainerClass() {
if (this.noAutoClass)
return this.acc;
const acc = {...this.acc};
if (this.inputGroup)
acc['input-group-item'] = true;
if (this.lcType == 'radio' || this.lcType == 'checkbox')
acc['form-check'] = true;
if (this.inputGroup && acc['form-check']) {
acc['input-group-item'] = false;
acc['form-check'] = false;
acc['input-group-text'] = true;
}
return acc;
},
lcType() {
if (!this.type)
return 'text';
return this.type.toLowerCase();
},
tag() {
switch (this.lcType) {
case 'textarea':
case 'select':
return this.lcType;
case 'datepicker':
return 'VueDatePicker';
case 'autocomplete':
return 'PvAutocomplete';
case 'uploadimage':
return 'UploadImage';
case 'uploadfile':
case 'uploaddms':
return 'UploadDms';
default:
return 'input';
}
},
validationClass() {
const classes = [];
if (this.valid)
classes.push('is-valid');
else if (this.valid === false)
classes.push('is-invalid');
if (!this.noAutoClass) {
let c = this.$attrs.class ? this.$attrs.class.split(' ') : [];
switch (this.lcType) {
// TODO(chris): complete list!
case 'select':
if (!c.includes('form-select'))
classes.push('form-select');
break;
case 'range':
if (!c.includes('form-range'))
classes.push('form-range');
break;
case 'radio':
case 'checkbox':
// TODO(chris): maybe different handling?
if (!c.includes('form-check-input') && !c.includes('btn-check'))
classes.push('form-check-input');
break;
case 'color':
if (!c.includes('form-control-color'))
classes.push('form-control-color');
if (!c.includes('form-control'))
classes.push('form-control');
break;
case 'autocomplete':
case 'datepicker':
classes.push('p-0');
classes.push('border-0');
if (!c.includes('form-control'))
classes.push('form-control');
break;
case 'text':
case 'number':
case 'password':
case 'textarea':
if (!c.includes('form-control'))
classes.push('form-control');
break;
}
}
return classes;
},
feedbackClass() {
if (!this.feedback || this.feedback === true)
return '';
if (!this.bsFeedback)
return {
'valid-tooltip': this.valid === true,
'invalid-tooltip': this.valid === false
};
return {
'valid-feedback': this.valid === true,
'invalid-feedback': this.valid === false
};
},
modelValueCmp: {
get() {
if (this.$attrs.modelValue === undefined)
return this.modelValueDummy;
return this.$attrs.modelValue;
},
set(v) {
if (this.$attrs.modelValue === undefined)
this.modelValueDummy = v;
this.$emit('update:modelValue', v);
}
},
idCmp() {
let uuid = this.$attrs.id;
if (this.lcType == 'datepicker')
uuid = this.$attrs.uid;
if (!uuid && this.$attrs.label)
uuid = 'fhc-form-input';
if (!uuid)
return undefined;
if (this.lcType == 'datepicker')
uuid = 'dp-input-' + uuid;
if (_uuid[uuid] === undefined)
_uuid[uuid] = 0;
return uuid + '-' + (_uuid[uuid]++);
}
},
methods: {
clearValidation() {
this.valid = undefined;
this.feedback = [];
},
clearValidationForThisName() {
if (this.valid === undefined)
return;
if (this.clearValidationForName && this.name)
this.clearValidationForName(this.name);
else
this.clearValidation();
},
setFeedback(valid, feedback) {
if (!feedback)
feedback = [];
if (!Array.isArray(feedback))
feedback = [feedback];
this.valid = valid;
// NOTE(chris): On a list of radios/checkboxes only add the feedback message to the last item
if (this.name && (this.lcType == 'radio' || this.lcType == 'checkbox')) {
const selector = 'input[type="' + this.lcType + '"][name="' + this.name + '"]';
if ([...this.$el.parentNode.querySelectorAll(selector)].pop() != this.$refs.input)
return;
}
this.feedback = feedback;
},
_loadComponents() {
if (this.tag == 'VueDatePicker' && !this._.components.VueDatePicker) {
this._.components.VueDatePicker = Vue.defineAsyncComponent(() => import("../vueDatepicker.js.php"));
} else if (this.tag == 'PvAutocomplete' && !this._.components.PvAutocomplete) {
this._.components.PvAutocomplete = Vue.defineAsyncComponent(() => import(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + "/public/js/components/primevue/autocomplete/autocomplete.esm.min.js"));
} else if (this.tag == 'UploadImage' && !this._.components.UploadImage) {
this._.components.UploadImage = Vue.defineAsyncComponent(() => import("./Upload/Image.js"));
} else if (this.tag == 'UploadDms' && !this._.components.UploadDms) {
this._.components.UploadDms = Vue.defineAsyncComponent(() => import("./Upload/Dms.js"));
}
}
},
beforeMount() {
this._loadComponents();
},
beforeUpdate() {
this._loadComponents();
},
mounted() {
if (this.registerToForm)
this.registerToForm(this);
},
template: `
<component :is="!hasContainer ? 'FhcFragment' : 'div'" class="position-relative" :class="autoContainerClass">
<label v-if="$attrs.label && lcType != 'radio' && lcType != 'checkbox'" :for="idCmp">{{$attrs.label}}</label>
<input v-if="tag == 'input'" :type="lcType" ref="input" v-model="modelValueCmp" v-bind="$attrs" :id="idCmp" :name="name" :class="validationClass" :modelValue="undefined" @input="clearValidationForThisName(); $emit('input', $event)">
<textarea v-else-if="tag == 'textarea'" ref="input" v-model="modelValueCmp" v-bind="$attrs" :id="idCmp" :name="name" :class="validationClass" :modelValue="undefined" @input="clearValidationForThisName(); $emit('input', $event)"></textarea>
<select v-else-if="tag == 'select'" ref="input" v-model="modelValueCmp" v-bind="$attrs" :id="idCmp" :name="name" :class="validationClass" :modelValue="undefined" @input="clearValidationForThisName(); $emit('input', $event)">
<slot></slot>
</select>
<component
v-else-if="tag == 'VueDatePicker'"
ref="input"
:is="tag"
:type="type"
v-model="modelValueCmp"
v-bind="$attrs"
:uid="idCmp ? idCmp.substr(9) : idCmp"
:name="name"
:class="validationClass"
:input-class-name=
"[...Object.entries({'form-control': !noAutoClass, 'is-valid': valid === true, 'is-invalid': valid === false}).reduce((a,[k,v]) => {if(v) a.push(k);return a}, []), ...($attrs['input-class-name'] ? $attrs['input-class-name'].split(' ') : [])].join(' ')"
@update:model-value="clearValidationForThisName"
>
<slot></slot>
</component>
<component
v-else-if="tag == 'PvAutocomplete'"
ref="input"
:is="tag"
:type="type"
v-model="modelValueCmp"
v-bind="$attrs"
:id="idCmp"
:input-props="{name}"
:class="validationClass"
:input-class="[...Object.entries({'form-control': !noAutoClass, 'is-valid': valid === true, 'is-invalid': valid === false}).reduce((a,[k,v]) => {if(v) a.push(k);return a}, []), ...($attrs['input-class'] ? $attrs['input-class'].split(' ') : [])].join(' ')"
@update:model-value="clearValidationForThisName"
>
<slot></slot>
</component>
<component
v-else-if="tag == 'UploadDms'"
ref="input"
:is="tag"
:type="type"
v-model="modelValueCmp"
v-bind="$attrs"
:id="idCmp"
:name="name"
:class="validationClass"
:input-class="validationClass"
:no-list="inputGroup"
@update:model-value="clearValidationForThisName"
>
<slot></slot>
</component>
<component
v-else
ref="input"
:is="tag"
:type="type"
v-model="modelValueCmp"
v-bind="$attrs"
:id="idCmp"
:name="name"
:class="validationClass"
@update:model-value="clearValidationForThisName"
>
<slot></slot>
</component>
<label v-if="$attrs.label && (lcType == 'radio' || lcType == 'checkbox')" :for="idCmp" :class="!noAutoClass && 'form-check-label'">{{$attrs.label}}</label>
<div v-if="valid !== undefined && feedback.length && !noFeedback" :class="feedbackClass">
<template v-for="(msg, i) in feedback" :key="i">
<hr v-if="i" class="m-0">
{{msg}}
</template>
</div>
</component>
`
}
+87
View File
@@ -0,0 +1,87 @@
export default {
emits: [
'update:modelValue'
],
props: {
modelValue: {
type: [ FileList, Array ],
required: true
},
multiple: Boolean,
id: String,
name: String,
inputClass: [String, Array, Object],
noList: Boolean
},
methods: {
stringifyFile(file) {
return JSON.stringify({
lastModified: file.lastModified,
lastModifiedDate: file.lastModifiedDate,
name: file.name,
size: file.size,
type: file.type
});
},
addFiles(event) {
if (!this.multiple)
return this.$emit('update:modelValue', event.target.files);
const dt = new DataTransfer();
const doubles = [];
for (var file of this.modelValue) {
dt.items.add(file);
doubles.push(this.stringifyFile(file));
}
for (var file of event.target.files) {
// NOTE(chris): deep check (with FileReader) would require an async function so we only check the basic attributes
if (doubles.indexOf(this.stringifyFile(file)) < 0)
dt.items.add(file);
}
this.$emit('update:modelValue', dt.files);
},
removeFile(id) {
const fileToRemove = Array.from(this.modelValue)[id];
const dt = new DataTransfer();
for (var file of this.modelValue) {
if (file !== fileToRemove)
dt.items.add(file);
}
this.$emit('update:modelValue', dt.files);
}
},
watch: {
modelValue(n) {
if (n instanceof FileList)
return this.$refs.upload.files = n;
const dt = new DataTransfer();
const dms = [];
for (var file of n) {
if (file instanceof File) {
dt.items.add(file);
} else {
const dmsFile = new File([JSON.stringify(file)], file.name, {
type: 'application/x.fhc-dms+json'
});
dt.items.add(dmsFile);
}
}
this.$emit('update:modelValue', dt.files);
}
},
template: `
<div class="form-upload-dms">
<input ref="upload" class="form-control" :class="inputClass" :id="id" :name="name" :multiple="multiple" type="file" @change="addFiles">
<ul v-if="modelValue.length && multiple && !noList" class="list-unstyled m-0">
<li v-for="(file, index) in modelValue" :key="index" class="d-flex mx-1 mt-1 align-items-start">
<span class="col-auto"><i class="fa fa-file me-1"></i></span>
<span class="col">{{ file.name }}</span>
<button class="col-auto btn btn-outline-secondary btn-p-0" @click="removeFile(index)">
<i class="fa fa-close"></i>
</button>
</li>
</ul>
</div>`
}
+62
View File
@@ -0,0 +1,62 @@
export default {
emits: [
'update:modelValue'
],
props: {
modelValue: String
},
computed: {
valueAsBase64DataString() {
if (!this.modelValue || this.modelValue.substring(0, 10) == 'data:image')
return this.modelValue;
return 'data:image/jpeg;charset=utf-8;base64,' + this.modelValue;
}
},
methods: {
openUploadDialog() {
this.$refs.fileInput.click();
},
pickFile() {
let file = this.$refs.fileInput.files;
if (file && file[0]) {
let reader = new FileReader();
reader.onload = e => {
this.$emit('update:modelValue', e.target.result);
}
reader.readAsDataURL(file[0]);
}
},
deleteImage() {
this.$emit('update:modelValue', '');
}
},
template: `
<div class="form-upload-image">
<template v-if="modelValue">
<img class="img-thumbnail" :src="valueAsBase64DataString" />
<div class="fotobutton">
<div class="d-grid gap-2 d-md-flex">
<button type="button" class="btn btn-outline-dark btn-sm" @click="deleteImage">
<i class="fa fa-close"></i>
</button>
<button type="button" class="btn btn-outline-dark btn-sm" @click="openUploadDialog">
<i class="fa fa-pen"></i>
</button>
</div>
</div>
</template>
<template v-else>
<slot>
<svg class="bd-placeholder-img img-thumbnail" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="A generic square placeholder image with a white border around it, making it resemble a photograph taken with an old instant camera: 200x200" preserveAspectRatio="xMidYMid slice" focusable="false"><title>A generic square placeholder image with a white border around it, making it resemble a photograph taken with an old instant camera</title><rect width="100%" height="100%" fill="#868e96"></rect><text x="50%" y="50%" fill="#dee2e6" dy=".3em"></text></svg>
</slot>
<div class="fotobutton-visible">
<div class="d-grid gap-2 d-md-flex">
<button type="button" class="btn btn-outline-dark btn-sm" @click="openUploadDialog">
<i class="fa fa-pen"></i>
</button>
</div>
</div>
</template>
<input :id="$attrs.id" class="d-none" type="file" ref="fileInput" @input="pickFile" accept="image/*">
</div>`
}
+41
View File
@@ -0,0 +1,41 @@
export default {
inject: [
'$registerToForm'
],
data() {
return {
feedback: {
success: [],
danger: []
}
};
},
methods: {
clearValidation() {
this.feedback = {
success: [],
danger: []
};
},
setFeedback(valid, feedback) {
if (!feedback)
feedback = [];
if (!Array.isArray(feedback))
feedback = [feedback];
const ts = Date.now();
this.feedback[valid ? 'success' : 'danger'] = feedback.map(msg => [msg, ts]);
}
},
mounted() {
if (this.$registerToForm)
this.$registerToForm(this);
},
template: `
<template v-for="(arr, key) in feedback" :key="key">
<div v-for="[msg, ts] in arr" :key="ts + msg" class="alert alert-dismissible fade show" :class="'alert-' + key" role="alert">
{{msg}}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
</template>
`
};
+5
View File
@@ -0,0 +1,5 @@
export default {
render() {
return (this.$slots && this.$slots.default) ? this.$slots.default() : null;
}
};
+65 -24
View File
@@ -6,12 +6,19 @@ export default {
accessibility
},
emits: [
'update:modelValue'
'update:modelValue',
'change',
'changed'
],
props: {
configUrl: String,
config: {
type: [String, Object],
required: true
},
default: String,
modelValue: [String, Number, Boolean, Array, Object, Date, Function, Symbol]
modelValue: [String, Number, Boolean, Array, Object, Date, Function, Symbol],
vertical: Boolean,
border: Boolean
},
data() {
return {
@@ -35,50 +42,84 @@ export default {
}
}
},
created() {
CoreRESTClient
.get(this.configUrl)
.then(result => CoreRESTClient.getData(result.data))
.then(result => {
const tabs = {};
// TODO(chris): check if result is array
Object.entries(result).forEach(([key, config]) => {
if (!config.component)
watch: {
config(n) {
this.initConfig(n);
}
},
methods: {
change(key) {
this.$emit("change", key)
this.current = key;
this.$nextTick(() => this.$emit("changed", key));
},
initConfig(config) {
if (!config)
return;
if (typeof config === 'string' || config instanceof String)
return CoreRESTClient.get(config)
.then(result => CoreRESTClient.getData(result.data))
.then(this.initConfig)
.catch(this.$fhcAlert.handleSystemError);
const tabs = {};
if (Array.isArray(config)) {
config.forEach((item, key) => {
if (!item.component)
return console.error('Component missing for ' + key);
tabs[key] = {
component: Vue.markRaw(Vue.defineAsyncComponent(() => import(config.component))),
title: config.title || key,
config: config.config,
component: Vue.markRaw(Vue.defineAsyncComponent(() => import(item.component))),
title: item.title || key,
config: item.config,
key
}
});
} else {
Object.entries(config).forEach(([key, item]) => {
if (!item.component)
return console.error('Component missing for ' + key);
tabs[key] = {
component: Vue.markRaw(Vue.defineAsyncComponent(() => import(item.component))),
title: item.title || key,
config: item.config,
key
}
});
}
if (this.current === null || !tabs[this.current]) {
if (tabs[this.default])
this.current = this.default;
else
this.current = Object.keys(tabs)[0];
this.tabs = tabs;
})
.catch(this.$fhcAlert.handleSystemError);
}
this.tabs = tabs;
}
},
created() {
this.initConfig(this.config);
},
template: `
<div class="fhc-tabs d-flex flex-column">
<div class="nav nav-tabs">
<div class="fhc-tabs d-flex" :class="vertical ? 'align-items-stretch gap-3' : (border ? 'flex-column' : 'flex-column gap-3')" v-if="Object.keys(tabs).length">
<div class="nav" :class="vertical ? 'nav-pills flex-column' : 'nav-tabs'">
<div
v-for="tab in tabs"
:key="tab.key"
class="nav-item nav-link"
:class="{active: tab.key == current}"
@click="current=tab.key"
@click="change(tab.key)"
:aria-current="tab.key == current ? 'page' : ''"
v-accessibility:tab
v-accessibility:tab.[vertical]
>
{{tab.title}}
</div>
</div>
<div style="flex: 1 1 0%; height: 0%" class="border-bottom border-start border-end overflow-auto p-3">
<div :style="vertical ? '' : 'flex: 1 1 0%; height: 0%'" class="overflow-auto" :class="vertical || !border ? '' : 'p-3 border-bottom border-start border-end'">
<keep-alive>
<component :is="currentTab.component" v-model="value" :config="currentTab.config"></component>
<component ref="current" :is="currentTab.component" v-model="value" :config="currentTab.config"></component>
</keep-alive>
</div>
</div>`
+15 -9
View File
@@ -143,9 +143,6 @@ export const CoreFilterCmpt = {
e.g. column with rowSelection checkboxes or with custom formatted action buttons */
col.visible = selectedFields.indexOf(col.field) >= 0 || fields.indexOf(col.field) == -1;
if (col.formatter == 'rowSelection')
col.visible = true;
if (col.hasOwnProperty('resizable'))
col.resizable = col.visible;
}
@@ -187,18 +184,27 @@ export const CoreFilterCmpt = {
methods: {
reloadTable() {
if (this.tableOnly)
// TODO check, vorher reload() gewesen
this.tabulator.setData();
else
this.getFilter();
},
initTabulator() {
async initTabulator() {
let placeholder = '< Phrasen Plugin not loaded! >';
if (this.$p) {
await this.$p.loadCategory('ui');
placeholder = this.$p.t('ui/keineDatenVorhanden');
}
// Define a default tabulator options in case it was not provided
let tabulatorOptions = {...{
height: 500,
layout: "fitColumns",
layout: "fitDataStretch",
movableColumns: true,
reactiveData: true
columnDefaults:{
tooltip: true,
},
placeholder,
reactiveData: true,
persistence: true
}, ...(this.tabulatorOptions || {})};
if (!this.tableOnly) {
@@ -276,9 +282,9 @@ export const CoreFilterCmpt = {
}
},
_updateTabulator() {
this.tabulatorHasSelector = this.filteredColumns.filter(el => el.formatter == 'rowSelection').length;
this.tabulator.setColumns(this.filteredColumns);
this.tabulator.setData(this.filteredData);
this.tabulatorHasSelector = this.filteredColumns.filter(el => el.formatter == 'rowSelection').length;
},
/**
*
@@ -476,7 +482,7 @@ export const CoreFilterCmpt = {
*
*/
handlerRemoveCustomFilter: function(event) {
filterId = event.currentTarget.getAttribute("href").substring(1);
let filterId = event.currentTarget.getAttribute("href").substring(1);
if (filterId === this.selectedFilter)
this.selectedFilter = null;
//
+54
View File
@@ -0,0 +1,54 @@
/**
* Copyright (C) 2022 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
props: {
title: '',
subtitle: '',
mainCols: {
type: Array,
default: []
},
asideCols: {
type: Array,
default: []
},
},
computed: {
mainGridCols() {
return this.mainCols.length > 0 ? `col-md-${this.mainCols[0]}` : "col-md-12";
},
asideGridCols() {
return this.asideCols.length > 0 ? `col-md-${this.asideCols[0]}` : "";
}
},
template: `
<div class="overflow-hidden">
<header v-if="title">
<h1 class="h2 mb-5">{{ title }}<span class="fhc-subtitle">{{ subtitle }}</span></h1>
</header>
<div class="row gx-5">
<main :class="mainGridCols">
<slot name="main">{{ mainGridCols }}</slot>
</main>
<aside v-if="asideCols.length > 0" :class="asideGridCols">
<slot name="aside">{{ asideGridCols }}</slot>
</aside>
</div>
</div>
`
};