conflict-nuxt-4/app/components/lazy-load/global/FormBuilder.vue
Baghi330 7892a7cefb 1
2026-02-14 10:41:53 +03:30

907 lines
30 KiB
Vue
Executable File

<template>
<div>
<div class="mt-3 text__15 text__green" v-if="description">
<span>{{ description }} </span>
</div>
<!-- tab/grouped mode -->
<template :key="renderMain" v-if="isArrayItems()">
<!-- start:horizontal mode -->
<template v-if="displayMode == 'horizontal'">
<div class="horizontal">
<!-- Tabs -->
<ul class="flex bg-gray-100 rounded-t-md overflow-hidden h-12">
<li
v-for="(groupItem, index) in localFormElements"
:key="'groupItem' + index"
@click="setTab(index)"
class="cursor-pointer mt-2"
>
<a
class="px-4 py-2 block relative"
:class="
currentTab === index
? 'bg-white text-gray-900 rounded-t-md'
: 'text-gray-600 hover:bg-gray-200'
"
>
{{ groupItem.title }}
</a>
</li>
</ul>
<!-- Tab content -->
<div
class="tab-content bg-white p-4 border border-t-0 border-gray-300 rounded-b-md"
>
<div
v-for="(groupItem, index) in localFormElements"
:key="'tabcontent' + index"
>
<div v-show="currentTab === index" class="px-3">
<form>
<div class="grid grid-cols-12 gap-4">
<div
v-for="(formElement, index1) in getGroupListElements(
groupItem.items,
)"
:key="formElement.key + index1"
:class="giveColClass('', formElement)"
>
<component
:is="
componentMap[
returnComponentNameDefault(formElement, groupItem)
]
"
:ref="`${refKeyBase}_${formElement.key}`"
:key="'element' + formElement.key + '_' + index1"
:formElement="
addValueToFormElement(formElement, groupItem)
"
:otherData="otherData"
:isReadOnly="readOnly"
:multipleMode="multipleMode"
:acceptType="acceptType"
:class="giveColClass('', formElement)"
:inputClass="giveColClass('input', formElement)"
:labelClass="'col-12'"
class="inside-entity align-items-center"
:classComponentName="classComponentName"
@updateComponentOption="
(ev) => updateComponentOption(ev, formElement)
"
@link-route-clicked="linkRouteClicked"
@take-value="
(ev) => takeValueChanged(ev, formElement, groupItem)
"
@on-upload-file="onUploadFile"
@action-affected-item="
(ev) => actionAffectedItem(ev, formElement)
"
@keydown="
formBuilderAction('formBuilderKeydown', $event)
"
/>
</div>
</div>
<!-- Submit Button -->
<div
v-if="isHorizontalButtonSubmit"
class="mt-4 flex justify-end"
>
<button
v-if="
isShowButtonAccordion(groupItem) &&
!isReadOnlyGroup(groupItem)
"
type="submit"
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
@click.prevent="saveGroupProperty(groupItem)"
>
{{ groupItem.submit_label ?? "ثبت" }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</template>
<!-- start:vertical mode -->
<div
class="flex flex-col md:flex-row"
v-else-if="displayMode == 'vertical'"
>
<!-- Sidebar -->
<div :class="listClass ?? 'md:w-1/4 w-1/3'">
<div
class="flex flex-col space-y-2"
role="tablist"
aria-orientation="vertical"
>
<button
v-for="(groupItem, index) in localFormElements"
:key="'groupItem' + index"
@click="setTab(index)"
class="px-4 py-2 text-left rounded-lg transition-colors duration-200"
:class="
currentTab == index
? 'bg-blue-500 text-white'
: 'bg-gray-100 text-gray-800 hover:bg-gray-200'
"
role="tab"
:aria-selected="currentTab == index"
>
{{ groupItem.title }}
</button>
</div>
</div>
<!-- Content -->
<div :class="mainClass ?? 'md:w-3/4 w-2/3'">
<div class="tab-content">
<div
class="tab-pane transition-all duration-300"
:class="currentTab == index ? 'block' : 'hidden'"
v-for="(groupItem, index) in localFormElements"
:key="'tab-pane' + index"
role="tabpanel"
>
<div class="ml-5 relative">
<div class="container-fluid">
<div class="grid grid-cols-12 gap-4">
<div
v-for="(formElement, index1) in getGroupListElements(
groupItem?.items,
)"
:key="index1"
:class="giveColClass('', formElement)"
>
<component
:is="
componentMap[
returnComponentNameDefault(formElement, groupItem)
]
"
:ref="`${refKeyBase}_${formElement.key}`"
:key="'element' + formElement.key + '_' + index1"
:formElement="
addValueToFormElement(formElement, groupItem)
"
:otherData="otherData"
:chartComponentName="formElement.chartComponentName"
:isReadOnly="readOnly"
:multipleMode="multipleMode"
:acceptType="acceptType"
:class="
giveColClass('', formElement) +
' inside-entity align-items-center'
"
:inputClass="giveColClass('input', formElement)"
:labelClass="'col-12'"
:classComponentName="classComponentName"
@treemap-node-click="
formBuilderAction('treemap-node-click', $event)
"
@updateComponentOption="
(ev) => updateComponentOption(ev, formElement)
"
@link-route-clicked="linkRouteClicked"
@take-value="
(ev) => takeValueChanged(ev, formElement, groupItem)
"
@on-upload-file="onUploadFile"
@action-affected-item="
(ev) => actionAffectedItem(ev, formElement)
"
@keydown="
formBuilderAction('formBuilderKeydown', $event)
"
:widthChart="'100%'"
:heightChart="'64dvh'"
:isOpenModal="false"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- accordion & accordion-show -->
<template
v-else-if="
displayMode == 'accordion' || displayMode == 'accordion-show'
"
>
<UAccordion
:items="accordionItems"
:type="displayMode === 'accordion-show' ? 'multiple' : 'single'"
:collapsible="displayMode !== 'accordion-show'"
:default-value="
displayMode === 'accordion-show'
? accordionItems.map((_, i) => String(i))
: []
"
:unmount-on-hide="false"
:ui="{
root: 'w-full space-y-2',
item: 'rounded-md border border-gray-200 dark:border-gray-700',
header: 'flex bg-gray-100 dark:bg-dark-primary-700 rounded-md',
trigger:
'group flex-1 items-center justify-between gap-2 py-3 px-4 font-medium text-start focus-visible:outline-primary cursor-pointer',
content:
'overflow-hidden data-[state=open]:animate-[accordion-down_200ms_ease-out] data-[state=closed]:animate-[accordion-up_200ms_ease-out]',
body: 'p-0',
label: 'text-base font-medium text-gray-900 dark:text-white',
trailingIcon:
'ms-auto size-5 text-gray-500 group-data-[state=open]:rotate-180 transition-transform',
}"
>
<template #content="{ item, index }">
<form @submit.prevent="saveGroupProperty(item.data)">
<div class="p-4">
<div v-if="item.data.is_array" class="mb-4">
<TableComponent
:key="`table-${index}`"
height="14em"
:tableColumns="item.data.table_columns"
:tagActions="item.data.tag_actions"
:items="localFormData[item.data.key]"
:itemKey="item.data.key"
:formElements="item.data.items"
:isReadOnly="isReadOnlyGroup(item.data)"
:showActions="!isReadOnlyGroup(item.data)"
@updateTableComponent="updateTableComponent"
@add-table-item="addTableItem(item.data)"
@edit-table-item="editTableItem($event, item.data)"
@action-table-item="actionTableItem($event, item.data)"
@delete-table-item="deleteTableItem($event, item.data)"
/>
</div>
<div v-else class="grid grid-cols-12 gap-4 mb-4">
<div
v-for="(formElement, idx) in getGroupListElements(
item.data.items,
)"
:key="`${formElement.key}-${idx}`"
:class="giveColClass('', formElement)"
>
<component
:is="
componentMap[
returnComponentNameDefault(formElement, item.data)
]
"
:ref="`${refKeyBase}_${formElement.key}`"
:formElement="
addValueToFormElement(formElement, item.data)
"
:otherData="otherData"
:isReadOnly="isReadOnlyGroup(item.data, formElement)"
:multipleMode="multipleMode"
:acceptType="acceptType"
:class="giveColClass('', formElement)"
:inputClass="giveColClass('input', formElement)"
classComponentName="inside-entity"
@updateComponentOption="
(ev) => updateComponentOption(ev, formElement)
"
@link-route-clicked="linkRouteClicked"
@take-value="
(ev) => takeValueChanged(ev, formElement, item.data)
"
@on-upload-file="onUploadFile"
@action-affected-item="
(ev) => actionAffectedItem(ev, formElement)
"
@openModalTags="openModalTags"
@keydown="formBuilderAction('formBuilderKeydown', $event)"
/>
</div>
</div>
<div
v-if="
isShowButtonAccordion(item.data) &&
!isReadOnlyGroup(item.data)
"
class="flex justify-end"
>
<UButton
type="submit"
variant="solid"
color="primary"
size="md"
class="px-4 cursor-pointer"
@click.prevent="saveGroupProperty(item.data)"
>
{{ item.data.submit_label || "ثبت" }}
</UButton>
</div>
</div>
</form>
</template>
</UAccordion>
</template>
<!-- default mode -->
<template v-else-if="displayMode == 'default'">
<div class="container-fluid">
<div class="row grid grid-cols-12 gap-4">
<div
v-for="(formElement, index) in getGroupListElements(
localFormElements,
)"
:key="formElement.key + index"
:class="giveColClass('', formElement)"
>
<component
:is="
componentMap[
returnComponentNameDefault(formElement, groupItem)
]
"
:ref="`${refKeyBase}_${formElement.key}`"
:key="index + formElement.key + render"
:formElement="
addValueToFormElement(formElement, localFormElements)
"
:otherData="otherData"
:isReadOnly="readOnly"
:multipleMode="multipleMode"
:acceptType="acceptType"
:class="giveColClass(formElement.type, formElement)"
:inputClass="giveColClass('input', formElement)"
:labelClass="giveColClass('label', formElement)"
class="inside-entity align-items-center"
:classComponentName="classComponentName"
@updateComponentOption="
(ev) => updateComponentOption(ev, formElement)
"
@on-action-handler="onActionHandler"
@link-route-clicked="linkRouteClicked"
@take-value="
(ev) => takeValueChanged(ev, formElement, localFormElements)
"
@on-upload-file="onUploadFile"
@action-affected-item="
(ev) => actionAffectedItem(ev, formElement)
"
@take-value-validate="
formBuilderAction('take-value-validate', $event)
"
@keydown="formBuilderAction('formBuilderKeydown', $event)"
/>
</div>
</div>
</div>
</template>
</template>
</div>
</template>
<script setup>
import {
ref,
reactive,
computed,
watch,
onMounted,
onBeforeUnmount,
nextTick,
} from "vue";
import { cloneDeep } from "lodash";
// Async Components (same as before, but using defineAsyncComponent explicitly)
// --- جایگزینی بخش import و تعریف componentMap ---
import FormInputComponent from "@/components/lazy-load/form-builder/FormInputComponent.vue";
import FormTextareaComponent from "@/components/lazy-load/form-builder/FormTextareaComponent.vue";
import FormSelectComponent from "@/components/lazy-load/form-builder/FormSelectComponent.vue";
import FormDateComponent from "@/components/lazy-load/form-builder/FormDateComponent.vue";
import FormUploadFilesComponent from "@/components/lazy-load/form-builder/FormUploadFilesComponent.vue";
// Transform localFormElements into AccordionItem[]
const accordionItems = computed(() => {
return localFormElements.value.map((groupItem, index) => ({
label: groupItem.title,
// Optional: you can add `value` if needed for controlled mode
// value: String(index),
// Pass original data for slots
data: groupItem,
}));
});
// سایر کامپوننت‌ها را هم به همین ترتیب اضافه کنید:
// import FormSelectComponent from '...';
// import FormLabelComponentReadOnly from '...';
const componentMap = {
FormInputComponent,
FormTextareaComponent,
FormSelectComponent,
FormDateComponent,
FormUploadFilesComponent,
// ... سایر کامپوننت‌هایی که استفاده می‌کنید
};
const props = defineProps({
refKeyBase: {
type: String,
default: "formbuilder",
},
displayMode: {
type: String,
default: "horizontal",
},
readOnly: {
type: Boolean,
default: false,
},
dataChart: {
type: Array,
default: () => [],
},
chartComponentName: {
type: String,
default: "",
},
formElements: {
type: Array,
default: () => [],
},
description: {
type: String,
default: "",
},
formData: {
type: Object,
default: () => ({}),
},
otherData: {
type: [Object, Array],
default: null,
},
previewMode: {
type: Boolean,
default: false,
},
classComponentName: {
type: String,
default: "",
},
multipleMode: {
type: Boolean,
default: true,
},
isHorizontalButtonSubmit: {
type: Boolean,
default: false,
},
acceptType: {
type: String,
default: "",
},
listClass: {
type: String,
default: "col-md-12",
},
mainClass: {
type: String,
default: "col-md-9",
},
});
// Emits (explicitly defined)
const emit = defineEmits(["form-builder-action"]);
function formBuilderAction(action, payload) {
emit("form-builder-action", {
action: action,
payload: payload,
});
}
// === Reactive State ===
const renderMain = ref(100);
const tableRender = ref(0);
const currentTab = ref(0);
const prevActiveTabIndex = ref(undefined);
const localFormElements = ref([]);
const localFormData = ref({});
const localFormDataChanged = ref({});
const activeRequest = ref([]);
let this_vm = getCurrentInstance();
// === Computed ===
const buttonText = computed(() => {
return localFormData.value?.id || localFormData.value?.guid
? "بروزرسانی"
: "افزودن";
});
// === Methods ===
const foundItemFormData = (key, index) => {
let item;
Object.keys(localFormData.value).forEach((nameItem) => {
if (nameItem === key) {
const arrayItemFound = localFormData.value[nameItem];
item = arrayItemFound?.[index];
}
});
return item;
};
const updateTableComponent = (event) => {
const { key, items } = event;
localFormData.value[key] = items;
localFormDataChanged.value[key] = items;
};
const isShowButtonAccordion = (item) => {
if (item.submit_label === "") return false;
return !props.readOnly;
};
const isReadOnlyGroup = (groupItem, item = undefined) => {
if (props.readOnly) return true;
let res = false;
if ("has_permission" in groupItem) res = !groupItem.has_permission;
if (item && "readOnly" in item && !res) res = item.readOnly;
return res;
};
const saveGroupProperty = (groupItem) => {
let changed_data = localFormDataChanged.value;
if (localFormDataChanged.value[groupItem.key]) {
changed_data = localFormDataChanged.value[groupItem.key];
}
let data = {};
let data_form = {};
if (!groupItem.type_data || groupItem.type_data === "normal") {
groupItem.items?.forEach((item) => {
if (changed_data[item.key]) data[item.key] = changed_data[item.key];
});
data_form = data;
} else {
data_form[groupItem.key] = changed_data;
}
formBuilderAction("saveFormGroup", data_form);
};
const onActionHandler = ({ action, key, value }) => {
const target = action.target;
updateValueFormData(target, value);
};
const isArrayItems = () => {
return Array.isArray(localFormElements.value);
};
const listenEventBus = ({ value, affectedTo }) => {
const refkey = `${props.refKeyBase}_${affectedTo.key}`;
const refs = this_vm.refs || {};
if (refs[refkey]) {
try {
const method =
affectedTo.action === "ufInitTextValue"
? (v) => refs[refkey][0]?.ufInitTextValue(v, true)
: (v) => refs[refkey][0]?.[affectedTo.action]?.(v);
method?.(value);
} catch (err) {
// silent
}
} else {
localFormData.value[affectedTo.key] = value;
localFormDataChanged.value[affectedTo.key] = value;
}
};
const setTab = (index) => {
if (prevActiveTabIndex.value !== undefined) {
localFormElements.value[prevActiveTabIndex.value].active = false;
}
currentTab.value = index;
prevActiveTabIndex.value = index;
formBuilderAction("setTab", index);
};
const getGroupListElements = (items) => {
if (!items) return [];
return items.filter((el) => !("ishide" in el) || el.ishide !== 1);
};
const findItemSchema = (key, items = undefined) => {
items = items || localFormElements.value;
for (let i = 0; i < items.length; i++) {
if (items[i]?.key === key) return items[i];
if (items[i].items) {
const found = findItemSchema(key, items[i].items);
if (found) return found;
}
}
return undefined;
};
const getSourceData = (itemData, key) => {
let elMom = itemData;
const elKeys = key.split("__");
for (let i = 0; i < elKeys.length - 1; i++) {
if (elMom?.[elKeys[i]]) {
elMom = elMom[elKeys[i]];
}
}
const finalKey = elKeys[elKeys.length - 1];
// console.log('getSourceData ', finalKey, elMom?.[finalKey], elMom);
return elMom?.[finalKey] ?? null;
};
const addValueToFormElement = (formElement, innerGroupItem) => {
const cloned = { ...formElement };
cloned["componentName"] = returnComponentNameDefault(cloned, innerGroupItem);
if (formElement.key) {
let formData = localFormData.value;
if (innerGroupItem && innerGroupItem?.type_data === "object") {
formData = localFormData.value[innerGroupItem.key] ?? {};
} else if (formElement.key == "file") {
formElement.key = "subtitle";
}
const value = getSourceData(formData, formElement.key);
cloned["value"] = value;
}
// console.log('addValueToFormElement ', formElement.key, cloned.value, cloned);
return cloned;
};
const returnComponentNameDefault = (item, groupItem) => {
const type = item.type;
// console.log("type ==> ", type);
const _readOnly = props.readOnly
? true
: groupItem
? isReadOnlyGroup(groupItem, item)
: false;
// console.log("_readOnly ==> ", _readOnly);
if (_readOnly) {
if (type === "textarea") return "FormTextareaComponent";
else if (type === "htmleditor") return "HtmlEditor";
else if (type === "tinyeditor") return "MyTinyMce";
else return "FormLabelComponentReadOnly";
}
switch (type) {
case "textarea":
return "FormTextareaComponent";
case "select":
return "FormSelectComponent";
case "string":
case "number":
case "float":
return "FormInputComponent";
case "label":
return "FormLabelComponentReadOnly";
case "tags":
return "tagsComponent";
case "selectTags":
return "SelectTagsComponent";
case "htmleditor":
return "HtmlEditor";
case "tinyeditor":
return "MyTinyMce";
case "date":
return "FormDateComponent";
case "range_date":
return "RangeDateComponent";
case "checkbox":
case "radio":
return "CheckboxComponent";
case "label_button":
return "LabelButtonComponent";
case "upload":
return "FormUploadFilesComponent";
case "multiFormSelectComponent":
return "MultiFormSelectComponent";
case "chart_content":
return "ChartContent";
default:
return "FormInputComponent";
}
};
const actionAffectedItem = ({ action, key, value }, schemaItem) => {
// console.log("actionAffectedItem ", action, key, value);
formBuilderAction("form-action-affected", { action, key, value, schemaItem });
};
const onUploadFile = (fileUploadData) => {
formBuilderAction("uploadFile", fileUploadData);
};
const takeValueObjectChanged = (value, formElement, innerGroupItem) => {
if (!innerGroupItem || innerGroupItem?.type_data !== "object") return false;
let root_object =
getSourceData(localFormData.value, innerGroupItem.key) ?? {};
root_object[formElement.key] = value;
localFormData.value[innerGroupItem.key] = root_object;
localFormDataChanged.value[innerGroupItem.key] = root_object;
return true;
};
const takeValueChanged = (value, schemaItem, innerGroupItem = null) => {
const key = schemaItem.key;
if (!takeValueObjectChanged(value, schemaItem, innerGroupItem)) {
if (typeof value === "string") value = value.trim() ?? null;
localFormData.value[key] = value;
localFormDataChanged.value[key] = value;
}
formBuilderAction("changeFormValues", { ...localFormDataChanged.value });
formBuilderAction("changeFormValueOne", { key, value });
};
const callItemMethod = (key, method, val) => {
const refkey = `${props.refKeyBase}_${key}`;
let refs = this_vm.refs || {};
if (refs[refkey]?.[0]?.[method]) {
refs[refkey][0][method](val);
}
};
const updateValueFormData = (key, value) => {
if (!value) return;
if (typeof value === "string") value = value.trim();
const refkey = `${props.refKeyBase}_${key}`;
let refs = this_vm.refs || {};
if (refs[refkey]?.initTextValue) {
refs[refkey].initTextValue(value);
}
localFormData.value[key] = value;
localFormDataChanged.value[key] = value;
formBuilderAction("changeFormValues", { ...localFormDataChanged.value });
formBuilderAction("changeFormValueOne", { key, value: value });
};
const giveColClass = (type, item) => {
// اگر کلاس دستی داده شده باشد استفاده می‌کنیم
// if (item.classes) {
// if (type === "") return item.classes;
// if (type === "label") return "col-auto";
// if (["input", "string", "textarea"].includes(type)) {
// return item.inputClass || item.classes;
// }
// }
// بررسی colSpan
const span = item.colSpan ?? 12; // پیش‌فرض کل عرض
const validSpan = Math.min(Math.max(span, 1), 12); // بین 1 تا 12
return `col-span-${validSpan}`;
};
const linkRouteClicked = (item) => {
if (!item.link_route) return;
const id_route = localFormData.value[item.link_route.id] ?? "";
const key = item.link_route.key;
const routeData = router.resolve({
name: item.link_route.name,
params: { id: id_route, key },
query: {},
});
window.open(routeData.href, "_blank");
};
// Table-specific handlers
const deleteTableItem = (event, schema) => {
const item = foundItemFormData(schema.key, event);
formBuilderAction("delete-table-item", {
schema,
item,
index: event,
});
};
const editTableItem = (event, schema) => {
const item = foundItemFormData(schema.key, event);
formBuilderAction("edit-table-item", {
schema,
item,
index: event,
});
};
const duplicateTableItem = (event, schema) => {
const item = foundItemFormData(schema.key, event);
formBuilderAction("duplicate-table-item", {
schema,
item,
index: event,
});
};
const actionTableItem = (event, schema) => {
emit(event.action, {
item: event.data.item,
index: event.data.index,
schema,
});
};
const addTableItem = (schema) => {
formBuilderAction("add-table-item", schema);
};
// Open modal for tags
const openModalTags = (item) => {
formBuilderAction("edit-property-tags", item);
};
// === Setup: Lifecycle & Watchers ===
import { getCurrentInstance } from "vue";
import { useNuxtApp } from "#app";
import { useRouter } from "vue-router";
const { $eventBus } = useNuxtApp();
const router = useRouter();
onMounted(() => {
localFormElements.value = props.formElements
? cloneDeep(props.formElements)
: [];
localFormData.value = props.formData ? cloneDeep(props.formData) : {};
renderMain.value++;
setTab(0);
// Event bus listener
if ($eventBus) {
$eventBus.on("catch-form-builder-event", listenEventBus);
}
});
onBeforeUnmount(() => {
if ($eventBus) {
$eventBus.off("catch-form-builder-event", listenEventBus);
}
});
// Watchers (deep + immediate)
watch(
() => props.formElements,
(newValue) => {
localFormElements.value = newValue ? cloneDeep(newValue) : [];
},
{ deep: true, immediate: true },
);
watch(
() => props.formData,
(newValue) => {
localFormData.value = newValue ? cloneDeep(newValue) : {};
},
{ deep: true, immediate: true },
);
// Expose methods if needed for parent refs (optional, if used externally)
defineExpose({
callItemMethod,
updateValueFormData,
setTab,
takeValueChanged,
});
</script>
<style lang="scss"></style>